Add support for non-message room events
This commit is contained in:
		
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							@@ -576,7 +576,7 @@ distdir: FORCE
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
clean: compiler_clean 
 | 
					clean: compiler_clean 
 | 
				
			||||||
	-$(DEL_FILE) $(OBJECTS)
 | 
						-$(DEL_FILE) $(OBJECTS)
 | 
				
			||||||
	-$(DEL_FILE) build/moc build/obj build/rcc src/python/__pycache__ src/python/events/__pycache__ build/resources.qrc build Makefile .qmake.stash src/python/__pycache__/__about__.cpython-36.pyc src/python/__pycache__/__init__.cpython-36.pyc src/python/__pycache__/app.cpython-36.pyc src/python/__pycache__/backend.cpython-36.pyc src/python/__pycache__/html_filter.cpython-36.pyc src/python/__pycache__/matrix_client.cpython-36.pyc src/python/events/__pycache__/__init__.cpython-36.pyc src/python/events/__pycache__/app.cpython-36.pyc src/python/events/__pycache__/event.cpython-36.pyc src/python/events/__pycache__/rooms.cpython-36.pyc src/python/events/__pycache__/rooms_timeline.cpython-36.pyc src/python/events/__pycache__/users.cpython-36.pyc
 | 
						-$(DEL_FILE) build/moc build/obj build/rcc src/python/__pycache__ src/python/events/__pycache__ build/resources.qrc build Makefile .qmake.stash src/python/__pycache__/__about__.cpython-36.pyc src/python/__pycache__/__init__.cpython-36.pyc src/python/__pycache__/app.cpython-36.pyc src/python/__pycache__/backend.cpython-36.pyc src/python/__pycache__/html_filter.cpython-36.pyc src/python/__pycache__/matrix_client.cpython-36.pyc src/python/events/__pycache__/__init__.cpython-36.pyc src/python/events/__pycache__/app.cpython-36.pyc src/python/events/__pycache__/event.cpython-36.pyc src/python/events/__pycache__/rooms.cpython-36.pyc src/python/events/__pycache__/rooms_timeline.cpython-36.pyc src/python/events/__pycache__/timeline.cpython-36.pyc src/python/events/__pycache__/users.cpython-36.pyc
 | 
				
			||||||
	-$(DEL_FILE) *~ core *.core
 | 
						-$(DEL_FILE) *~ core *.core
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
from typing import Dict, Optional
 | 
					from enum import auto
 | 
				
			||||||
 | 
					from typing import Dict, Optional, Sequence, Type, Union
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dataclasses import dataclass, field
 | 
					from dataclasses import dataclass, field
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .event import Event
 | 
					import nio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .event import AutoStrEnum, Event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
@@ -38,3 +41,52 @@ class RoomMemberUpdated(Event):
 | 
				
			|||||||
class RoomMemberDeleted(Event):
 | 
					class RoomMemberDeleted(Event):
 | 
				
			||||||
    room_id: str  = field()
 | 
					    room_id: str  = field()
 | 
				
			||||||
    user_id: str  = field()
 | 
					    user_id: str  = field()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Timeline
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ContentType(AutoStrEnum):
 | 
				
			||||||
 | 
					    html     = auto()
 | 
				
			||||||
 | 
					    image    = auto()
 | 
				
			||||||
 | 
					    audio    = auto()
 | 
				
			||||||
 | 
					    video    = auto()
 | 
				
			||||||
 | 
					    file     = auto()
 | 
				
			||||||
 | 
					    location = auto()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class TimelineEventReceived(Event):
 | 
				
			||||||
 | 
					    event_type: Type[nio.Event] = field()
 | 
				
			||||||
 | 
					    room_id:    str             = field()
 | 
				
			||||||
 | 
					    event_id:   str             = field()
 | 
				
			||||||
 | 
					    sender_id:  str             = field()
 | 
				
			||||||
 | 
					    date:       datetime        = field()
 | 
				
			||||||
 | 
					    content:    str             = field()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content_type:  ContentType = ContentType.html
 | 
				
			||||||
 | 
					    is_local_echo: bool        = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    show_name_line: bool                       = False
 | 
				
			||||||
 | 
					    translatable:   Union[bool, Sequence[str]] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    target_user_id: Optional[str] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def from_nio(cls, room: nio.rooms.MatrixRoom, ev: nio.Event, **fields
 | 
				
			||||||
 | 
					                ) -> "TimelineEventReceived":
 | 
				
			||||||
 | 
					        return cls(
 | 
				
			||||||
 | 
					            event_type = type(ev),
 | 
				
			||||||
 | 
					            room_id    = room.room_id,
 | 
				
			||||||
 | 
					            event_id   = ev.event_id,
 | 
				
			||||||
 | 
					            sender_id  = ev.sender,
 | 
				
			||||||
 | 
					            date       = datetime.fromtimestamp(ev.server_timestamp / 1000),
 | 
				
			||||||
 | 
					            target_user_id = getattr(ev, "state_key", None),
 | 
				
			||||||
 | 
					            **fields
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class TimelineMessageReceived(TimelineEventReceived):
 | 
				
			||||||
 | 
					    show_name_line: bool                       = True
 | 
				
			||||||
 | 
					    translatable:   Union[bool, Sequence[str]] = False
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +0,0 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					 | 
				
			||||||
from enum import auto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from dataclasses import dataclass, field
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from .event import AutoStrEnum, Event
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class EventType(AutoStrEnum):
 | 
					 | 
				
			||||||
    text     = auto()
 | 
					 | 
				
			||||||
    html     = auto()
 | 
					 | 
				
			||||||
    file     = auto()
 | 
					 | 
				
			||||||
    image    = auto()
 | 
					 | 
				
			||||||
    audio    = auto()
 | 
					 | 
				
			||||||
    video    = auto()
 | 
					 | 
				
			||||||
    location = auto()
 | 
					 | 
				
			||||||
    notice   = auto()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@dataclass
 | 
					 | 
				
			||||||
class TimelineEvent(Event):
 | 
					 | 
				
			||||||
    type:          EventType = field()
 | 
					 | 
				
			||||||
    room_id:       str       = field()
 | 
					 | 
				
			||||||
    event_id:      str       = field()
 | 
					 | 
				
			||||||
    sender_id:     str       = field()
 | 
					 | 
				
			||||||
    date:          datetime  = field()
 | 
					 | 
				
			||||||
    is_local_echo: bool      = field()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@dataclass
 | 
					 | 
				
			||||||
class HtmlMessageReceived(TimelineEvent):
 | 
					 | 
				
			||||||
    content: str = field()
 | 
					 | 
				
			||||||
@@ -1,9 +1,10 @@
 | 
				
			|||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
 | 
					import html
 | 
				
			||||||
import inspect
 | 
					import inspect
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
import logging as log
 | 
					import logging as log
 | 
				
			||||||
import platform
 | 
					import platform
 | 
				
			||||||
from contextlib import suppress
 | 
					from contextlib import suppress
 | 
				
			||||||
from datetime import datetime
 | 
					 | 
				
			||||||
from types import ModuleType
 | 
					from types import ModuleType
 | 
				
			||||||
from typing import Dict, Optional, Type
 | 
					from typing import Dict, Optional, Type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,7 +13,7 @@ from nio.rooms import MatrixRoom
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from . import __about__
 | 
					from . import __about__
 | 
				
			||||||
from .events import rooms, users
 | 
					from .events import rooms, users
 | 
				
			||||||
from .events.rooms_timeline import EventType, HtmlMessageReceived
 | 
					from .events.rooms import TimelineEventReceived, TimelineMessageReceived
 | 
				
			||||||
from .html_filter import HTML_FILTER
 | 
					from .html_filter import HTML_FILTER
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -162,17 +163,157 @@ class MatrixClient(nio.AsyncClient):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Callbacks for nio events
 | 
					    # Callbacks for nio events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def onRoomMessageText(self, room: MatrixRoom, ev: nio.RoomMessageText
 | 
					    # Special %tokens for event contents:
 | 
				
			||||||
                               ) -> None:
 | 
					    # %S = sender's displayname
 | 
				
			||||||
        is_html = ev.format == "org.matrix.custom.html"
 | 
					    # %T = target (ev.state_key)'s displayname
 | 
				
			||||||
        filter_ = HTML_FILTER.filter
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        HtmlMessageReceived(
 | 
					    async def onRoomMessageText(self, room, ev) -> None:
 | 
				
			||||||
            type          = EventType.html if is_html else EventType.text,
 | 
					        co = HTML_FILTER.filter(
 | 
				
			||||||
            room_id       = room.room_id,
 | 
					            ev.formatted_body
 | 
				
			||||||
            event_id      = ev.event_id,
 | 
					            if ev.format == "org.matrix.custom.html" else html.escape(ev.body)
 | 
				
			||||||
            sender_id     = ev.sender,
 | 
					 | 
				
			||||||
            date          = datetime.fromtimestamp(ev.server_timestamp / 1000),
 | 
					 | 
				
			||||||
            is_local_echo = False,
 | 
					 | 
				
			||||||
            content       = filter_(ev.formatted_body) if is_html else ev.body,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        TimelineMessageReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomCreateEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = "%S allowed users on other matrix servers to join this room." \
 | 
				
			||||||
 | 
					             if ev.federate else \
 | 
				
			||||||
 | 
					             "%S blocked users on other matrix servers from joining this room."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomGuestAccessEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        allowed = "allowed" if ev.guest_access else "forbad"
 | 
				
			||||||
 | 
					        co      = f"%S {allowed} guests to join the room."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomJoinRulesEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        access = "public" if ev.join_rule == "public" else "invite-only"
 | 
				
			||||||
 | 
					        co     = f"%S made the room {access}."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomHistoryVisibilityEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        if ev.history_visibility == "shared":
 | 
				
			||||||
 | 
					            to = "all room members"
 | 
				
			||||||
 | 
					        elif ev.history_visibility == "world_readable":
 | 
				
			||||||
 | 
					            to = "any member or outsider"
 | 
				
			||||||
 | 
					        elif ev.history_visibility == "joined":
 | 
				
			||||||
 | 
					            to = "all room members, since the time they joined"
 | 
				
			||||||
 | 
					        elif ev.history_visibility == "invited":
 | 
				
			||||||
 | 
					            to = "all room members, since the time they were invited"
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            to = "???"
 | 
				
			||||||
 | 
					            log.warning("Invalid visibility - %s",
 | 
				
			||||||
 | 
					                        json.dumps(ev.__dict__, indent=4))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        co = f"%S made future room history visible to {to}."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onPowerLevelsEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = "%S changed the room's permissions."  # TODO: improve
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def _get_room_member_event_content(self, ev) -> str:
 | 
				
			||||||
 | 
					        prev            = ev.prev_content
 | 
				
			||||||
 | 
					        prev_membership = prev["membership"] if prev else None
 | 
				
			||||||
 | 
					        now             = ev.content
 | 
				
			||||||
 | 
					        membership      = now["membership"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not prev or membership != prev_membership:
 | 
				
			||||||
 | 
					            reason = f" Reason: {now['reason']}" if now.get("reason") else ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if membership == "join":
 | 
				
			||||||
 | 
					                did = "accepted" if prev and prev_membership == "invite" else \
 | 
				
			||||||
 | 
					                      "declined"
 | 
				
			||||||
 | 
					                return f"%S {did} their invitation."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if membership == "invite":
 | 
				
			||||||
 | 
					                return f"%S invited %T to the room."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if membership == "leave":
 | 
				
			||||||
 | 
					                if ev.state_key == ev.sender:
 | 
				
			||||||
 | 
					                    return (
 | 
				
			||||||
 | 
					                        f"%S declined their invitation.{reason}"
 | 
				
			||||||
 | 
					                        if prev and prev_membership == "invite" else
 | 
				
			||||||
 | 
					                        f"%S left the room.{reason}"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return (
 | 
				
			||||||
 | 
					                    f"%S withdrew %T's invitation.{reason}"
 | 
				
			||||||
 | 
					                    if prev and prev_membership == "invite" else
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    f"%S unbanned %T from the room.{reason}"
 | 
				
			||||||
 | 
					                    if prev and prev_membership == "ban" else
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    f"%S kicked out %T from the room.{reason}"
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if membership == "ban":
 | 
				
			||||||
 | 
					                return f"%S banned %T from the room.{reason}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        changed = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if prev and now["avatar_url"] != prev["avatar_url"]:
 | 
				
			||||||
 | 
					            changed.append("profile picture")  # TODO: <img>s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if prev and now["displayname"] != prev["displayname"]:
 | 
				
			||||||
 | 
					            changed.append('display name from "{}" to "{}"'.format(
 | 
				
			||||||
 | 
					                prev["displayname"] or ev.state_key,
 | 
				
			||||||
 | 
					                now["displayname"] or ev.state_key,
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if changed:
 | 
				
			||||||
 | 
					            return "%S changed their {}.".format(" and ".join(changed))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        log.warning("Invalid member event - %s",
 | 
				
			||||||
 | 
					                    json.dumps(ev.__dict__, indent=4))
 | 
				
			||||||
 | 
					        return "%S ???"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomMemberEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = await self._get_room_member_event_content(ev)
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomAliasEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = f"%S set the room's main address to {ev.canonical_alias}."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomNameEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = f"%S changed the room's name to \"{ev.name}\"."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomTopicEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = f"%S changed the room's topic to \"{ev.topic}\"."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onRoomEncryptionEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = f"%S turned on encryption for this room."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onOlmEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = f"%S hasn't sent your device the keys to decrypt this message."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onMegolmEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        await self.onOlmEvent(room, ev)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onBadEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = f"%S sent a malformed event."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def onUnknownBadEvent(self, room, ev) -> None:
 | 
				
			||||||
 | 
					        co = f"%S sent an event this client doesn't understand."
 | 
				
			||||||
 | 
					        TimelineEventReceived.from_nio(room, ev, content=co)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,27 +55,27 @@ HColumnLayout {
 | 
				
			|||||||
                Layout.fillHeight: true
 | 
					                Layout.fillHeight: true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            TypingMembersBar {}
 | 
					            //TypingMembersBar {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            InviteBanner {
 | 
					//            InviteBanner {
 | 
				
			||||||
                visible: category === "Invites"
 | 
					                //visible: category === "Invites"
 | 
				
			||||||
                inviter: roomInfo.inviter
 | 
					                //inviter: roomInfo.inviter
 | 
				
			||||||
            }
 | 
					            //}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            UnknownDevicesBanner {
 | 
					            //UnknownDevicesBanner {
 | 
				
			||||||
                visible: category == "Rooms" && hasUnknownDevices
 | 
					                //visible: category == "Rooms" && hasUnknownDevices
 | 
				
			||||||
            }
 | 
					            //}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            SendBox {
 | 
					            SendBox {
 | 
				
			||||||
                id: sendBox
 | 
					                id: sendBox
 | 
				
			||||||
                visible: category == "Rooms" && ! hasUnknownDevices
 | 
					                visible: category == "Rooms" && ! hasUnknownDevices
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            LeftBanner {
 | 
					            //LeftBanner {
 | 
				
			||||||
                visible: category === "Left"
 | 
					                //visible: category === "Left"
 | 
				
			||||||
                leftEvent: roomInfo.leftEvent
 | 
					                //leftEvent: roomInfo.leftEvent
 | 
				
			||||||
            }
 | 
					            //}
 | 
				
			||||||
        }
 | 
					        //}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//        RoomSidePane {
 | 
					//        RoomSidePane {
 | 
				
			||||||
            //id: roomSidePane
 | 
					            //id: roomSidePane
 | 
				
			||||||
@@ -145,4 +145,5 @@ HColumnLayout {
 | 
				
			|||||||
            //Layout.maximumWidth: parent.width
 | 
					            //Layout.maximumWidth: parent.width
 | 
				
			||||||
        //}
 | 
					        //}
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,11 +7,18 @@ Row {
 | 
				
			|||||||
    spacing: standardSpacing / 2
 | 
					    spacing: standardSpacing / 2
 | 
				
			||||||
    layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
 | 
					    layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function textHueForName(name) { // TODO: move
 | 
				
			||||||
 | 
					        return Qt.hsla(avatar.hueFromName(name),
 | 
				
			||||||
 | 
					                       HStyle.displayName.saturation,
 | 
				
			||||||
 | 
					                       HStyle.displayName.lightness,
 | 
				
			||||||
 | 
					                       1)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    HAvatar {
 | 
					    HAvatar {
 | 
				
			||||||
        id: avatar
 | 
					        id: avatar
 | 
				
			||||||
        hidden: combine
 | 
					        hidden: combine
 | 
				
			||||||
        name: senderInfo.displayName || stripUserId(model.senderId)
 | 
					        name: senderInfo.displayName || stripUserId(model.senderId)
 | 
				
			||||||
        dimension: 48
 | 
					        dimension: model.showNameLine ? 48 : 28
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Rectangle {
 | 
					    Rectangle {
 | 
				
			||||||
@@ -33,16 +40,13 @@ Row {
 | 
				
			|||||||
            anchors.fill: parent
 | 
					            anchors.fill: parent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            HLabel {
 | 
					            HLabel {
 | 
				
			||||||
                height: combine ? 0 : implicitHeight
 | 
					 | 
				
			||||||
                width: parent.width
 | 
					                width: parent.width
 | 
				
			||||||
 | 
					                height: model.showNameLine && ! combine ? implicitHeight : 0
 | 
				
			||||||
                visible: height > 0
 | 
					                visible: height > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                id: nameLabel
 | 
					                id: nameLabel
 | 
				
			||||||
                text: senderInfo.displayName || model.senderId
 | 
					                text: senderInfo.displayName || model.senderId
 | 
				
			||||||
                color: Qt.hsla(avatar.hueFromName(avatar.name),
 | 
					                color: textHueForName(avatar.name)
 | 
				
			||||||
                               HStyle.displayName.saturation,
 | 
					 | 
				
			||||||
                               HStyle.displayName.lightness,
 | 
					 | 
				
			||||||
                               1)
 | 
					 | 
				
			||||||
                elide: Text.ElideRight
 | 
					                elide: Text.ElideRight
 | 
				
			||||||
                maximumLineCount: 1
 | 
					                maximumLineCount: 1
 | 
				
			||||||
                horizontalAlignment: isOwn ? Text.AlignRight : Text.AlignLeft
 | 
					                horizontalAlignment: isOwn ? Text.AlignRight : Text.AlignLeft
 | 
				
			||||||
@@ -53,10 +57,49 @@ Row {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            HRichLabel {
 | 
					            HRichLabel {
 | 
				
			||||||
 | 
					                function escapeHtml(text) {  // TODO: move this
 | 
				
			||||||
 | 
					                    return text.replace("&", "&")
 | 
				
			||||||
 | 
					                               .replace("<", "<")
 | 
				
			||||||
 | 
					                               .replace(">", ">")
 | 
				
			||||||
 | 
					                               .replace('"', """)
 | 
				
			||||||
 | 
					                               .replace("'", "'")
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                function translate(text) {
 | 
				
			||||||
 | 
					                    if (model.translatable == false) { return text }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    text = text.replace(
 | 
				
			||||||
 | 
					                        "%S",
 | 
				
			||||||
 | 
					                        "<font color='" + nameLabel.color + "'>" +
 | 
				
			||||||
 | 
					                        escapeHtml(senderInfo.displayName || model.senderId) +
 | 
				
			||||||
 | 
					                        "</font>"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    var name = models.users.getUser(
 | 
				
			||||||
 | 
					                        chatPage.userId, model.targetUserId
 | 
				
			||||||
 | 
					                    ).displayName
 | 
				
			||||||
 | 
					                    var sid = avatar.stripUserId(model.targetUserId || "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    text = text.replace(
 | 
				
			||||||
 | 
					                        "%T",
 | 
				
			||||||
 | 
					                        "<font color='" + textHueForName(name || sid) + "'>" +
 | 
				
			||||||
 | 
					                        escapeHtml(name || model.targetUserId) +
 | 
				
			||||||
 | 
					                        "</font>"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    text = qsTr(text)
 | 
				
			||||||
 | 
					                    if (model.translatable == true) { return text }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Else, model.translatable should be an array of args
 | 
				
			||||||
 | 
					                    for (var i = 0; model.translatable.length; i++) {
 | 
				
			||||||
 | 
					                        text = text.arg(model.translatable[i])
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                width: parent.width
 | 
					                width: parent.width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                id: contentLabel
 | 
					                id: contentLabel
 | 
				
			||||||
                text: model.content +
 | 
					                text: translate(model.content) +
 | 
				
			||||||
                      "  <font size=" + HStyle.fontSize.small +
 | 
					                      "  <font size=" + HStyle.fontSize.small +
 | 
				
			||||||
                      "px color=" + HStyle.chat.message.date + ">" +
 | 
					                      "px color=" + HStyle.chat.message.date + ">" +
 | 
				
			||||||
                      Qt.formatDateTime(model.date, "hh:mm:ss") +
 | 
					                      Qt.formatDateTime(model.date, "hh:mm:ss") +
 | 
				
			||||||
@@ -64,8 +107,6 @@ Row {
 | 
				
			|||||||
                      (model.isLocalEcho ?
 | 
					                      (model.isLocalEcho ?
 | 
				
			||||||
                       " <font size=" + HStyle.fontSize.small +
 | 
					                       " <font size=" + HStyle.fontSize.small +
 | 
				
			||||||
                       "px>⏳</font>" : "")
 | 
					                       "px>⏳</font>" : "")
 | 
				
			||||||
                textFormat: model.type == "text" ?
 | 
					 | 
				
			||||||
                            Text.PlainText : Text.RichText
 | 
					 | 
				
			||||||
                color: HStyle.chat.message.body
 | 
					                color: HStyle.chat.message.body
 | 
				
			||||||
                wrapMode: Text.Wrap
 | 
					                wrapMode: Text.Wrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,10 +14,6 @@ Column {
 | 
				
			|||||||
                roomEventListView.model.get(index + 1) : null
 | 
					                roomEventListView.model.get(index + 1) : null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function getIsMessage(type) {
 | 
					 | 
				
			||||||
        return true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    property var previousItem: getPreviousItem()
 | 
					    property var previousItem: getPreviousItem()
 | 
				
			||||||
    signal reloadPreviousItem()
 | 
					    signal reloadPreviousItem()
 | 
				
			||||||
    onReloadPreviousItem: previousItem = getPreviousItem()
 | 
					    onReloadPreviousItem: previousItem = getPreviousItem()
 | 
				
			||||||
@@ -26,18 +22,14 @@ Column {
 | 
				
			|||||||
    Component.onCompleted:
 | 
					    Component.onCompleted:
 | 
				
			||||||
        senderInfo = models.users.getUser(chatPage.userId, senderId)
 | 
					        senderInfo = models.users.getUser(chatPage.userId, senderId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //readonly property bool isMessage: ! model.type.match(/^event.*/)
 | 
					 | 
				
			||||||
    readonly property bool isMessage: getIsMessage(model.type)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    readonly property bool isOwn: chatPage.userId === senderId
 | 
					    readonly property bool isOwn: chatPage.userId === senderId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    readonly property bool isFirstEvent: model.type == "eventCreate"
 | 
					    readonly property bool isFirstEvent: model.event_type == "RoomCreateEvent"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    readonly property bool combine:
 | 
					    readonly property bool combine:
 | 
				
			||||||
        previousItem &&
 | 
					        previousItem &&
 | 
				
			||||||
        ! talkBreak &&
 | 
					        ! talkBreak &&
 | 
				
			||||||
        ! dayBreak &&
 | 
					        ! dayBreak &&
 | 
				
			||||||
        getIsMessage(previousItem.type) === isMessage &&
 | 
					 | 
				
			||||||
        previousItem.senderId === senderId &&
 | 
					        previousItem.senderId === senderId &&
 | 
				
			||||||
        minsBetween(previousItem.date, model.date) <= 5
 | 
					        minsBetween(previousItem.date, model.date) <= 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,14 +67,13 @@ Column {
 | 
				
			|||||||
        width: roomEventDelegate.width
 | 
					        width: roomEventDelegate.width
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Item {
 | 
					    Item { // TODO: put this in Daybreak.qml?
 | 
				
			||||||
        visible: dayBreak
 | 
					        visible: dayBreak
 | 
				
			||||||
        width: parent.width
 | 
					        width: parent.width
 | 
				
			||||||
        height: topPadding
 | 
					        height: topPadding
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Loader {
 | 
					    MessageContent {
 | 
				
			||||||
        source: isMessage ? "MessageContent.qml" : "EventContent.qml"
 | 
					 | 
				
			||||||
        anchors.right: isOwn ? parent.right : undefined
 | 
					        anchors.right: isOwn ? parent.right : undefined
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,154 +1,3 @@
 | 
				
			|||||||
function getEventText(type, dict) {
 | 
					 | 
				
			||||||
    switch (type) {
 | 
					 | 
				
			||||||
        case "RoomCreateEvent":
 | 
					 | 
				
			||||||
            return (dict.federate ? "allowed" : "blocked") +
 | 
					 | 
				
			||||||
                   " users on other matrix servers " +
 | 
					 | 
				
			||||||
                   (dict.federate ? "to join" : "from joining") +
 | 
					 | 
				
			||||||
                   " this room."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomGuestAccessEvent":
 | 
					 | 
				
			||||||
            return (dict.guest_access === "can_join" ? "allowed " : "forbad") +
 | 
					 | 
				
			||||||
                   "guests to join the room."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomJoinRulesEvent":
 | 
					 | 
				
			||||||
            return "made the room " +
 | 
					 | 
				
			||||||
                   (dict.join_rule === "public." ? "public" : "invite only.")
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomHistoryVisibilityEvent":
 | 
					 | 
				
			||||||
            return getHistoryVisibilityEventText(dict)
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "PowerLevelsEvent":
 | 
					 | 
				
			||||||
            return "changed the room's permissions."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomMemberEvent":
 | 
					 | 
				
			||||||
            return getMemberEventText(dict)
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomAliasEvent":
 | 
					 | 
				
			||||||
            return "set the room's main address to " +
 | 
					 | 
				
			||||||
                   dict.canonical_alias + "."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomNameEvent":
 | 
					 | 
				
			||||||
            return "changed the room's name to \"" + dict.name + "\"."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomTopicEvent":
 | 
					 | 
				
			||||||
            return "changed the room's topic to \"" + dict.topic + "\"."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "RoomEncryptionEvent":
 | 
					 | 
				
			||||||
            return "turned on encryption for this room."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "OlmEvent":
 | 
					 | 
				
			||||||
        case "MegolmEvent":
 | 
					 | 
				
			||||||
            return "hasn't sent your device the keys to decrypt this message."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        default:
 | 
					 | 
				
			||||||
            console.log(type + "\n" + JSON.stringify(dict, null, 4) + "\n")
 | 
					 | 
				
			||||||
            return "did something this client does not understand."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        //case "CallEvent":  TODO
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getHistoryVisibilityEventText(dict) {
 | 
					 | 
				
			||||||
    switch (dict.history_visibility) {
 | 
					 | 
				
			||||||
        case "shared":
 | 
					 | 
				
			||||||
            var end = "all room members."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "world_readable":
 | 
					 | 
				
			||||||
            var end = "any member or outsider."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "joined":
 | 
					 | 
				
			||||||
            var end = "all room members, since the point they joined."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        case "invited":
 | 
					 | 
				
			||||||
            var end = "all room members, since the point they were invited."
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return "made future history visible to " + end
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getStateDisplayName(dict) {
 | 
					 | 
				
			||||||
    // The dict.content.displayname may be outdated, prefer
 | 
					 | 
				
			||||||
    // retrieving it fresh
 | 
					 | 
				
			||||||
    return Backend.users.get(dict.state_key).displayName.value
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getMemberEventText(dict) {
 | 
					 | 
				
			||||||
    var info = dict.content, prev = dict.prev_content
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (! prev || (info.membership != prev.membership)) {
 | 
					 | 
				
			||||||
        var reason = info.reason ? (" Reason: " + info.reason) : ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        switch (info.membership) {
 | 
					 | 
				
			||||||
            case "join":
 | 
					 | 
				
			||||||
                return prev && prev.membership === "invite" ?
 | 
					 | 
				
			||||||
                       "accepted the invitation." : "joined the room."
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case "invite":
 | 
					 | 
				
			||||||
                return "invited " + getStateDisplayName(dict) + " to the room."
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case "leave":
 | 
					 | 
				
			||||||
                if (dict.state_key === dict.sender) {
 | 
					 | 
				
			||||||
                    return (prev && prev.membership === "invite" ?
 | 
					 | 
				
			||||||
                            "declined the invitation." : "left the room.") +
 | 
					 | 
				
			||||||
                           reason
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var name = getStateDisplayName(dict)
 | 
					 | 
				
			||||||
                return (prev && prev.membership === "invite" ?
 | 
					 | 
				
			||||||
                        "withdrew " + name + "'s invitation." :
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        prev && prev.membership == "ban" ?
 | 
					 | 
				
			||||||
                        "unbanned " + name + " from the room." :
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        "kicked out " + name  + " from the room.") +
 | 
					 | 
				
			||||||
                       reason
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            case "ban":
 | 
					 | 
				
			||||||
                var name = getStateDisplayName(dict)
 | 
					 | 
				
			||||||
                return "banned " + name + " from the room." + reason
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var changed = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (prev && (info.avatar_url != prev.avatar_url)) {
 | 
					 | 
				
			||||||
        changed.push("profile picture")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (prev && (info.displayname != prev.displayname)) {
 | 
					 | 
				
			||||||
        changed.push("display name from \"" +
 | 
					 | 
				
			||||||
                     (prev.displayname || dict.state_key) + '" to "' +
 | 
					 | 
				
			||||||
                     (info.displayname || dict.state_key) + '"')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (changed.length > 0) {
 | 
					 | 
				
			||||||
        return "changed their " + changed.join(" and ") + "."
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return ""
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getLeftBannerText(leftEvent) {
 | 
					function getLeftBannerText(leftEvent) {
 | 
				
			||||||
    if (! leftEvent) {
 | 
					    if (! leftEvent) {
 | 
				
			||||||
        return "You are not member of this room."
 | 
					        return "You are not member of this room."
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,4 +2,3 @@
 | 
				
			|||||||
Qt.include("app.js")
 | 
					Qt.include("app.js")
 | 
				
			||||||
Qt.include("users.js")
 | 
					Qt.include("users.js")
 | 
				
			||||||
Qt.include("rooms.js")
 | 
					Qt.include("rooms.js")
 | 
				
			||||||
Qt.include("rooms_timeline.js")
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,3 +59,25 @@ function onRoomMemberUpdated(room_id, user_id, typing) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function onRoomMemberDeleted(room_id, user_id) {
 | 
					function onRoomMemberDeleted(room_id, user_id) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onTimelineEventReceived(
 | 
				
			||||||
 | 
					    event_type, room_id, event_id, sender_id, date, content,
 | 
				
			||||||
 | 
					    content_type, is_local_echo, show_name_line, translatable, target_user_id
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    models.timelines.upsert({"eventId": event_id}, {
 | 
				
			||||||
 | 
					        "eventType":    py.getattr(event_type, "__name__"),
 | 
				
			||||||
 | 
					        "roomId":       room_id,
 | 
				
			||||||
 | 
					        "eventId":      event_id,
 | 
				
			||||||
 | 
					        "senderId":     sender_id,
 | 
				
			||||||
 | 
					        "date":         date,
 | 
				
			||||||
 | 
					        "content":      content,
 | 
				
			||||||
 | 
					        "contentType":  content,
 | 
				
			||||||
 | 
					        "isLocalEcho":  is_local_echo,
 | 
				
			||||||
 | 
					        "showNameLine": show_name_line,
 | 
				
			||||||
 | 
					        "translatable": translatable,
 | 
				
			||||||
 | 
					        "targetUserId": target_user_id
 | 
				
			||||||
 | 
					    }, true, 1000)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var onTimelineMessageReceived = onTimelineEventReceived
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
function onHtmlMessageReceived(type, room_id, event_id, sender_id, date,
 | 
					 | 
				
			||||||
                               is_local_echo, content) {
 | 
					 | 
				
			||||||
    models.timelines.upsert({"eventId": event_id}, {
 | 
					 | 
				
			||||||
        "type":        type,
 | 
					 | 
				
			||||||
        "roomId":      room_id,
 | 
					 | 
				
			||||||
        "eventId":     event_id,
 | 
					 | 
				
			||||||
        "senderId":    sender_id,
 | 
					 | 
				
			||||||
        "date":        date,
 | 
					 | 
				
			||||||
        "isLocalEcho": is_local_echo,
 | 
					 | 
				
			||||||
        "content":     content,
 | 
					 | 
				
			||||||
    }, true, 1000)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user