adds reactions
This commit is contained in:
parent
565508b217
commit
f5691fd8be
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@ __pycache__
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
venv
|
venv
|
||||||
|
sitecustomize.py
|
||||||
|
|
||||||
*.qmlc
|
*.qmlc
|
||||||
*.jsc
|
*.jsc
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
remote_pdb >= 2.0.0, < 3
|
remote_pdb >= 2.0.0, < 3
|
||||||
pdbpp >= 0.10.2, < 0.11
|
pdbpp >= 0.10.2, < 0.11
|
||||||
devtools >= 0.4.0, < 0.5
|
devtools >= 0.12.0, < 0.13
|
||||||
|
|
||||||
mypy >= 0.812, < 0.900
|
mypy >= 1.7.0, < 1.8
|
||||||
flake8 >= 3.8.4, < 4
|
flake8 >= 6.1.0, < 7
|
||||||
flake8-isort >= 4.0.0, < 5
|
flake8-isort >= 6.1.0, < 7
|
||||||
flake8-bugbear >= 20.1.4, < 21
|
flake8-bugbear >= 23.12.0, < 24
|
||||||
flake8-commas >= 2.0.0, < 3
|
flake8-commas >= 2.0.0, < 3
|
||||||
flake8-comprehensions >= 3.3.0, < 4
|
flake8-comprehensions >= 3.3.0, < 4
|
||||||
flake8-executable >= 2.0.4, < 3
|
flake8-executable >= 2.0.4, < 3
|
||||||
flake8-logging-format >= 0.6.0, < 0.7
|
flake8-logging-format >= 0.9.0, < 1
|
||||||
flake8-pie >= 0.6.1, < 0.7
|
flake8-pie >= 0.16.0, < 1
|
||||||
flake8-quotes >= 3.2.0, < 4
|
flake8-quotes >= 3.2.0, < 4
|
||||||
flake8-colors >= 0.1.6, < 0.2
|
flake8-colors >= 0.1.6, < 0.2
|
||||||
|
|
|
@ -2,6 +2,7 @@ Pillow >= 7.0.0, < 9
|
||||||
aiofiles >= 0.4.0, < 24.0.0
|
aiofiles >= 0.4.0, < 24.0.0
|
||||||
appdirs >= 1.4.4, < 2
|
appdirs >= 1.4.4, < 2
|
||||||
cairosvg >= 2.4.2, < 3
|
cairosvg >= 2.4.2, < 3
|
||||||
|
emoji >= 2.0, < 3.0
|
||||||
filetype >= 1.0.7, < 2
|
filetype >= 1.0.7, < 2
|
||||||
html_sanitizer >= 1.9.1, < 2
|
html_sanitizer >= 1.9.1, < 2
|
||||||
lxml >= 4.5.1, < 5
|
lxml >= 4.5.1, < 5
|
||||||
|
@ -14,7 +15,7 @@ redbaron >= 0.9.2, < 1
|
||||||
hsluv >= 5.0.0, < 6
|
hsluv >= 5.0.0, < 6
|
||||||
simpleaudio >= 1.0.4, < 2
|
simpleaudio >= 1.0.4, < 2
|
||||||
dbus-python >= 1.2.16, < 2; platform_system == "Linux"
|
dbus-python >= 1.2.16, < 2; platform_system == "Linux"
|
||||||
matrix-nio[e2e] >= 0.20.1, < 1.0.0
|
matrix-nio[e2e] >= 0.22.0, < 0.24
|
||||||
|
|
||||||
async_generator >= 1.10, < 2; python_version < "3.7"
|
async_generator >= 1.10, < 2; python_version < "3.7"
|
||||||
dataclasses >= 0.6, < 0.7; python_version < "3.7"
|
dataclasses >= 0.6, < 0.7; python_version < "3.7"
|
||||||
|
|
|
@ -26,6 +26,7 @@ from urllib.parse import urlparse
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import cairosvg
|
import cairosvg
|
||||||
|
import emoji
|
||||||
import nio
|
import nio
|
||||||
from nio.crypto import AsyncDataT as UploadData
|
from nio.crypto import AsyncDataT as UploadData
|
||||||
from nio.crypto import async_generator_from_data
|
from nio.crypto import async_generator_from_data
|
||||||
|
@ -225,6 +226,9 @@ class MatrixClient(nio.AsyncClient):
|
||||||
self.unassigned_event_last_read_by: DefaultDict[str, Dict[str, int]] =\
|
self.unassigned_event_last_read_by: DefaultDict[str, Dict[str, int]] =\
|
||||||
DefaultDict(dict)
|
DefaultDict(dict)
|
||||||
|
|
||||||
|
# {reacted_event_id: {emoji: [user_id]}}
|
||||||
|
self.unassigned_reaction_events: Dict[str, Dict[str, List[str]]] = {}
|
||||||
|
|
||||||
self.push_rules: nio.PushRulesEvent = nio.PushRulesEvent()
|
self.push_rules: nio.PushRulesEvent = nio.PushRulesEvent()
|
||||||
self.ignored_user_ids: Set[str] = set()
|
self.ignored_user_ids: Set[str] = set()
|
||||||
|
|
||||||
|
@ -359,7 +363,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
timeout = 10,
|
timeout = 10,
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
log.warn("%s timed out", self.user_id)
|
log.warning("%s timed out", self.user_id)
|
||||||
|
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
|
@ -377,7 +381,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
account.max_upload_size = future.result() or 0
|
account.max_upload_size = future.result() or 0
|
||||||
except MatrixError:
|
except MatrixError:
|
||||||
trace = traceback.format_exc().rstrip()
|
trace = traceback.format_exc().rstrip()
|
||||||
log.warn(
|
log.warning(
|
||||||
"On %s server config retrieval: %s", self.user_id, trace,
|
"On %s server config retrieval: %s", self.user_id, trace,
|
||||||
)
|
)
|
||||||
self.server_config_task = asyncio.ensure_future(
|
self.server_config_task = asyncio.ensure_future(
|
||||||
|
@ -2421,6 +2425,49 @@ class MatrixClient(nio.AsyncClient):
|
||||||
self.backend.notification_avatar_cache[mxc] = path
|
self.backend.notification_avatar_cache[mxc] = path
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
async def register_reaction(
|
||||||
|
self,
|
||||||
|
room: nio.MatrixRoom,
|
||||||
|
ev: nio.ReactionEvent,
|
||||||
|
event_id: str = "",
|
||||||
|
**fields,
|
||||||
|
) -> Event:
|
||||||
|
"""Register/update a reaction."""
|
||||||
|
reacts_to = ev.reacts_to
|
||||||
|
key = ev.key
|
||||||
|
sender = ev.sender
|
||||||
|
|
||||||
|
model = self.models[self.user_id, room.room_id, "events"]
|
||||||
|
reacts_to_event = model.get(reacts_to)
|
||||||
|
if not reacts_to_event: # local echo
|
||||||
|
for item in model.values():
|
||||||
|
if item.event_id == reacts_to:
|
||||||
|
reacts_to_event = item
|
||||||
|
|
||||||
|
# message is already loaded: update reactions instantly
|
||||||
|
if reacts_to_event:
|
||||||
|
reactions = reacts_to_event.reactions
|
||||||
|
if key not in reactions:
|
||||||
|
reactions[key] = {"hint": emoji.demojize(key), "users": []}
|
||||||
|
if sender not in reactions[key]["users"]:
|
||||||
|
reactions[key]["users"].append(sender)
|
||||||
|
reacts_to_event.set_fields(reactions=reactions)
|
||||||
|
reacts_to_event.notify_change("reactions")
|
||||||
|
|
||||||
|
# message is not loaded yet: register the reaction for later update
|
||||||
|
else:
|
||||||
|
registry = self.unassigned_reaction_events
|
||||||
|
if reacts_to not in registry:
|
||||||
|
registry[reacts_to] = {}
|
||||||
|
if key not in registry[reacts_to]:
|
||||||
|
registry[reacts_to][key] = []
|
||||||
|
if sender not in registry[reacts_to][key]:
|
||||||
|
registry[reacts_to][key].append(sender)
|
||||||
|
|
||||||
|
await self.register_nio_event(
|
||||||
|
room, ev, event_id, type_specifier=TypeSpecifier.Reaction,
|
||||||
|
content=key, hidden=True, **fields,
|
||||||
|
)
|
||||||
|
|
||||||
async def register_nio_event(
|
async def register_nio_event(
|
||||||
self,
|
self,
|
||||||
|
@ -2498,6 +2545,19 @@ class MatrixClient(nio.AsyncClient):
|
||||||
item.id = f"echo-{tx_id}"
|
item.id = f"echo-{tx_id}"
|
||||||
self.event_to_echo_ids[ev.event_id] = item.id
|
self.event_to_echo_ids[ev.event_id] = item.id
|
||||||
|
|
||||||
|
reactions = self.unassigned_reaction_events.get(item.id, {})
|
||||||
|
for key, senders in reactions.items(): # update reactions
|
||||||
|
if key not in item.reactions:
|
||||||
|
item.reactions[key] = {
|
||||||
|
"hint": emoji.demojize(key),
|
||||||
|
"users": [],
|
||||||
|
}
|
||||||
|
item.reactions[key]["users"] += senders
|
||||||
|
if ev.source.get("type") == "m.reaction" \
|
||||||
|
and ev.source.get("unsigned", {}).get("redacted_by"):
|
||||||
|
item.type_specifier = TypeSpecifier.ReactionRedaction
|
||||||
|
item.hidden = True
|
||||||
|
|
||||||
model[item.id] = item
|
model[item.id] = item
|
||||||
await self.set_room_last_event(room.room_id, item)
|
await self.set_room_last_event(room.room_id, item)
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,11 @@ ZERO_DATE = datetime.fromtimestamp(0)
|
||||||
class TypeSpecifier(AutoStrEnum):
|
class TypeSpecifier(AutoStrEnum):
|
||||||
"""Enum providing clarification of purpose for some matrix events."""
|
"""Enum providing clarification of purpose for some matrix events."""
|
||||||
|
|
||||||
Unset = auto()
|
Unset = auto()
|
||||||
ProfileChange = auto()
|
ProfileChange = auto()
|
||||||
MembershipChange = auto()
|
MembershipChange = auto()
|
||||||
|
Reaction = auto()
|
||||||
|
ReactionRedaction = auto()
|
||||||
|
|
||||||
|
|
||||||
class PingStatus(AutoStrEnum):
|
class PingStatus(AutoStrEnum):
|
||||||
|
@ -349,6 +351,7 @@ class Event(ModelItem):
|
||||||
sender_name: str = field()
|
sender_name: str = field()
|
||||||
sender_avatar: str = field()
|
sender_avatar: str = field()
|
||||||
fetch_profile: bool = False
|
fetch_profile: bool = False
|
||||||
|
hidden: bool = False
|
||||||
|
|
||||||
content: str = ""
|
content: str = ""
|
||||||
inline_content: str = ""
|
inline_content: str = ""
|
||||||
|
@ -356,6 +359,8 @@ class Event(ModelItem):
|
||||||
links: List[str] = field(default_factory=list)
|
links: List[str] = field(default_factory=list)
|
||||||
mentions: List[Tuple[str, str]] = field(default_factory=list)
|
mentions: List[Tuple[str, str]] = field(default_factory=list)
|
||||||
|
|
||||||
|
reactions: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||||
|
|
||||||
type_specifier: TypeSpecifier = TypeSpecifier.Unset
|
type_specifier: TypeSpecifier = TypeSpecifier.Unset
|
||||||
|
|
||||||
target_id: str = ""
|
target_id: str = ""
|
||||||
|
|
|
@ -242,6 +242,12 @@ class NioCallbacks:
|
||||||
await self.onRoomMessageMedia(room, ev)
|
await self.onRoomMessageMedia(room, ev)
|
||||||
|
|
||||||
|
|
||||||
|
async def onReactionEvent(
|
||||||
|
self, room: nio.MatrixRoom, ev: nio.ReactionEvent,
|
||||||
|
) -> None:
|
||||||
|
await self.client.register_reaction(room, ev, ev.event_id)
|
||||||
|
|
||||||
|
|
||||||
async def onRedactionEvent(
|
async def onRedactionEvent(
|
||||||
self, room: nio.MatrixRoom, ev: nio.RedactionEvent,
|
self, room: nio.MatrixRoom, ev: nio.RedactionEvent,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -260,12 +266,35 @@ class NioCallbacks:
|
||||||
await self.client.register_nio_room(room)
|
await self.client.register_nio_room(room)
|
||||||
return
|
return
|
||||||
|
|
||||||
event.source.source["content"] = {}
|
event_type = event.source.source.get("type")
|
||||||
|
if not event_type == "m.reaction":
|
||||||
|
event.source.source["content"] = {}
|
||||||
event.source.source["unsigned"] = {
|
event.source.source["unsigned"] = {
|
||||||
"redacted_by": ev.event_id,
|
"redacted_by": ev.event_id,
|
||||||
"redacted_because": ev.source,
|
"redacted_because": ev.source,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Remove reactions
|
||||||
|
if event_type == "m.reaction":
|
||||||
|
relates_to = event.source.source.get(
|
||||||
|
"content", {}).get("m.relates_to", {})
|
||||||
|
reacted_event_id = relates_to.get("event_id")
|
||||||
|
reacted_event = model.get(reacted_event_id)
|
||||||
|
key = relates_to.get("key")
|
||||||
|
sender = ev.source.get("sender")
|
||||||
|
# Remove reactions from registry
|
||||||
|
reg = self.client.unassigned_reaction_events.get(
|
||||||
|
reacted_event_id)
|
||||||
|
if reg and key in reg and sender in reg[key]:
|
||||||
|
reg[key].remove(sender)
|
||||||
|
# Remove reactions from loaded messages
|
||||||
|
if reacted_event and key in reacted_event.reactions:
|
||||||
|
if sender in reacted_event.reactions[key]:
|
||||||
|
reacted_event.reactions[key].remove(sender)
|
||||||
|
if not reacted_event.reactions[key]:
|
||||||
|
del reacted_event.reactions[key]
|
||||||
|
reacted_event.notify_change('reactions')
|
||||||
|
|
||||||
await self.onRedactedEvent(
|
await self.onRedactedEvent(
|
||||||
room,
|
room,
|
||||||
nio.RedactedEvent.from_dict(event.source.source),
|
nio.RedactedEvent.from_dict(event.source.source),
|
||||||
|
|
|
@ -49,6 +49,8 @@ HRowLayout {
|
||||||
">"
|
">"
|
||||||
) + "</font></font></a>"
|
) + "</font></font></a>"
|
||||||
|
|
||||||
|
readonly property var reactions: model.reactions
|
||||||
|
|
||||||
readonly property bool pureMedia: ! contentText && linksRepeater.count
|
readonly property bool pureMedia: ! contentText && linksRepeater.count
|
||||||
|
|
||||||
readonly property bool hoveredSelectable: contentHover.hovered
|
readonly property bool hoveredSelectable: contentHover.hovered
|
||||||
|
@ -298,6 +300,8 @@ HRowLayout {
|
||||||
|
|
||||||
linksRepeater.summedWidth +
|
linksRepeater.summedWidth +
|
||||||
(pureMedia ? 0 : parent.leftPadding + parent.rightPadding),
|
(pureMedia ? 0 : parent.leftPadding + parent.rightPadding),
|
||||||
|
|
||||||
|
reactionsRow.width
|
||||||
)
|
)
|
||||||
height: contentColumn.height
|
height: contentColumn.height
|
||||||
radius: theme.chat.message.radius
|
radius: theme.chat.message.radius
|
||||||
|
@ -361,6 +365,94 @@ HRowLayout {
|
||||||
Layout.preferredHeight: item ? item.height : -1
|
Layout.preferredHeight: item ? item.height : -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: reactionsRow
|
||||||
|
|
||||||
|
spacing: 10
|
||||||
|
bottomPadding: 7
|
||||||
|
leftPadding: 10
|
||||||
|
rightPadding: 10
|
||||||
|
Layout.alignment: onRight ? Qt.AlignRight : Qt.AlignLeft
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: reactionsRepeater
|
||||||
|
|
||||||
|
model: {
|
||||||
|
const reactions = Object.entries(
|
||||||
|
JSON.parse(eventDelegate.currentModel.reactions));
|
||||||
|
return reactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: reactionItem
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
readonly property var icon: modelData[0]
|
||||||
|
readonly property var hint: modelData[1]["hint"]
|
||||||
|
readonly property var users: modelData[1]["users"]
|
||||||
|
|
||||||
|
width: reactionContent.width
|
||||||
|
height: theme.fontSize.normal + 10
|
||||||
|
radius: width / 2
|
||||||
|
color: theme.colors.strongBackground
|
||||||
|
border.color: theme.colors.accentBackground
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: reactionContent
|
||||||
|
spacing: 5
|
||||||
|
topPadding: 3
|
||||||
|
leftPadding: 10
|
||||||
|
rightPadding: 10
|
||||||
|
Text {
|
||||||
|
id: reactionIcon
|
||||||
|
color: theme.colors.brightText
|
||||||
|
font.pixelSize: theme.fontSize.normal
|
||||||
|
font.family: theme.fontFamily.sans
|
||||||
|
text: parent.parent.icon
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
id: reactionCounter
|
||||||
|
color: theme.colors.brightText
|
||||||
|
font.pixelSize: theme.fontSize.normal
|
||||||
|
font.family: theme.fontFamily.sans
|
||||||
|
text: parent.parent.users.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: reactionItemMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
onEntered: { reactionTooltip.visible = true }
|
||||||
|
onExited: { reactionTooltip.visible = false }
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
HToolTip {
|
||||||
|
id: reactionTooltip
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
label.textFormat: HLabel.StyledText
|
||||||
|
text: {
|
||||||
|
const members =
|
||||||
|
ModelStore.get(chat.userId, chat.roomId, "members")
|
||||||
|
|
||||||
|
const lines = [parent.hint]
|
||||||
|
for (const userId of parent.users) {
|
||||||
|
const member = members.find(userId)
|
||||||
|
|
||||||
|
const by = utils.coloredNameHtml(
|
||||||
|
member ? member.display_name: userId, userId,
|
||||||
|
)
|
||||||
|
lines.push(qsTr("%1").arg(by))
|
||||||
|
}
|
||||||
|
return lines.join("<br>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HSpacer {}
|
HSpacer {}
|
||||||
|
|
|
@ -72,20 +72,26 @@ HColumnLayout {
|
||||||
eventList.toggleCheck(model.index)
|
eventList.toggleCheck(model.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visible: !model.hidden
|
||||||
width: eventList.width - eventList.leftMargin - eventList.rightMargin
|
width: eventList.width - eventList.leftMargin - eventList.rightMargin
|
||||||
|
|
||||||
// Needed because of eventList's MouseArea which steals the
|
// Needed because of eventList's MouseArea which steals the
|
||||||
// HSelectableLabel's MouseArea hover events
|
// HSelectableLabel's MouseArea hover events
|
||||||
onCursorShapeChanged: eventList.cursorShape = cursorShape
|
onCursorShapeChanged: eventList.cursorShape = cursorShape
|
||||||
|
|
||||||
Component.onCompleted: if (model.fetch_profile)
|
Component.onCompleted: {
|
||||||
fetchProfilesFutureId = py.callClientCoro(
|
if (model.fetch_profile)
|
||||||
chat.userId,
|
fetchProfilesFutureId = py.callClientCoro(
|
||||||
"get_event_profiles",
|
chat.userId,
|
||||||
[chat.roomId, model.id],
|
"get_event_profiles",
|
||||||
// The if avoids segfault if eventDelegate is already destroyed
|
[chat.roomId, model.id],
|
||||||
() => { if (eventDelegate) fetchProfilesFutureId = "" }
|
// The if avoids segfault if eventDelegate is already destroyed
|
||||||
)
|
() => { if (eventDelegate) fetchProfilesFutureId = "" }
|
||||||
|
)
|
||||||
|
// Workaround for hiding messages of certain types
|
||||||
|
if (!eventDelegate.visible)
|
||||||
|
eventDelegate.height = 0
|
||||||
|
}
|
||||||
|
|
||||||
Component.onDestruction:
|
Component.onDestruction:
|
||||||
if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId)
|
if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId)
|
||||||
|
|
|
@ -266,10 +266,26 @@ Rectangle {
|
||||||
highlightRangeMode = previous
|
highlightRangeMode = previous
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusPreviousVisibleMessage() {
|
||||||
|
incrementCurrentIndex()
|
||||||
|
let lastIndex = -1
|
||||||
|
while ( currentIndex != lastIndex && model.get(currentIndex).hidden ) {
|
||||||
|
lastIndex = currentIndex
|
||||||
|
incrementCurrentIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function focusPreviousMessage() {
|
function focusPreviousMessage() {
|
||||||
currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ?
|
currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ?
|
||||||
focusCenterMessage() :
|
focusCenterMessage() :
|
||||||
incrementCurrentIndex()
|
focusPreviousVisibleMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNextVisibleMessage() {
|
||||||
|
decrementCurrentIndex()
|
||||||
|
while ( currentIndex > -1 && model.get(currentIndex).hidden ) {
|
||||||
|
decrementCurrentIndex()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusNextMessage() {
|
function focusNextMessage() {
|
||||||
|
@ -279,7 +295,7 @@ Rectangle {
|
||||||
eventList.currentIndex === 0 ?
|
eventList.currentIndex === 0 ?
|
||||||
eventList.currentIndex = -1 :
|
eventList.currentIndex = -1 :
|
||||||
|
|
||||||
decrementCurrentIndex()
|
focusNextVisibleMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
function copySelectedDelegates() {
|
function copySelectedDelegates() {
|
||||||
|
@ -332,7 +348,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
function canCombine(item, itemAfter) {
|
function canCombine(item, itemAfter) {
|
||||||
if (! item || ! itemAfter) return false
|
if (! item || ! itemAfter || item.hidden) return false
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
! canTalkBreak(item, itemAfter) &&
|
! canTalkBreak(item, itemAfter) &&
|
||||||
|
|
|
@ -11,13 +11,13 @@ QtObject {
|
||||||
property bool keyboardFlicking: false
|
property bool keyboardFlicking: false
|
||||||
|
|
||||||
readonly property var imageExtensions: [
|
readonly property var imageExtensions: [
|
||||||
"bmp", "gif", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", "xbm", "xpm",
|
"bmp", "gif", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", "xbm", "xpm",
|
||||||
"tiff", "webp", "svg",
|
"tiff", "webp", "svg",
|
||||||
]
|
]
|
||||||
|
|
||||||
readonly property var videoExtensions: [
|
readonly property var videoExtensions: [
|
||||||
"3gp", "avi", "flv", "m4p", "m4v", "mkv", "mov", "mp4",
|
"3gp", "avi", "flv", "m4p", "m4v", "mkv", "mov", "mp4",
|
||||||
"mpeg", "mpg", "ogv", "qt", "vob", "webm", "wmv", "yuv",
|
"mpeg", "mpg", "ogv", "qt", "vob", "webm", "wmv", "yuv",
|
||||||
]
|
]
|
||||||
|
|
||||||
readonly property var audioExtensions: [
|
readonly property var audioExtensions: [
|
||||||
|
@ -214,6 +214,31 @@ QtObject {
|
||||||
const unknownMsg = type === "RoomMessageUnknown"
|
const unknownMsg = type === "RoomMessageUnknown"
|
||||||
const sender = coloredNameHtml(ev.sender_name, ev.sender_id)
|
const sender = coloredNameHtml(ev.sender_name, ev.sender_id)
|
||||||
|
|
||||||
|
if (ev.type_specifier === "Reaction") {
|
||||||
|
let name = coloredNameHtml(
|
||||||
|
ev.sender_name, ev.sender_id, "", true,
|
||||||
|
)
|
||||||
|
let reaction = ev.content
|
||||||
|
|
||||||
|
return qsTr(
|
||||||
|
`<font color="${theme.chat.message.noticeBody}">` +
|
||||||
|
name + ": " + reaction +
|
||||||
|
"</font>"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (ev.type_specifier === "ReactionRedaction") {
|
||||||
|
let name = coloredNameHtml(
|
||||||
|
ev.sender_name, ev.sender_id, "", true,
|
||||||
|
)
|
||||||
|
let reaction = ev.content
|
||||||
|
|
||||||
|
return qsTr(
|
||||||
|
`<font color="${theme.chat.message.noticeBody}">` +
|
||||||
|
name + " removed a reaction" +
|
||||||
|
"</font>"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "RoomMessageEmote")
|
if (type === "RoomMessageEmote")
|
||||||
return ev.content.match(/^\s*<(p|h[1-6])>/) ?
|
return ev.content.match(/^\s*<(p|h[1-6])>/) ?
|
||||||
ev.content.replace(/(^\s*<(p|h[1-6])>)/, `$1${sender} `) :
|
ev.content.replace(/(^\s*<(p|h[1-6])>)/, `$1${sender} `) :
|
||||||
|
|
Loading…
Reference in New Issue
Block a user