adds reactions
This commit is contained in:
		
							
								
								
									
										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} `) :
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user