Fix changes requested (#101)

- Fix code formatting issues.
- Document `Presence`.
- Improve `Presence.__lt__()` performance by
  defining a dict outside the method.
- Make presence ball radius relative to uiScale
  and configurable from theme.
This commit is contained in:
vslg 2020-07-10 11:59:26 -03:00 committed by miruka
parent 43df8fd60b
commit 5abdc1f779
13 changed files with 111 additions and 81 deletions

View File

@ -82,6 +82,9 @@ class Backend:
we managed. Every client is logged to one matrix account. we managed. Every client is logged to one matrix account.
media_cache: A matrix media cache for downloaded files. media_cache: A matrix media cache for downloaded files.
presences: A `{user_id: Presence}` dict for storing presence info about
matrix users registered on Mirage.
""" """
def __init__(self) -> None: def __init__(self) -> None:

View File

@ -232,9 +232,10 @@ class MatrixClient(nio.AsyncClient):
async def login( async def login(
self, password: str, self,
device_name: str = "", password: str,
order: Optional[int] = None, device_name: str = "",
order: Optional[int] = None,
) -> None: ) -> None:
"""Login to the server using the account's password.""" """Login to the server using the account's password."""
@ -242,12 +243,14 @@ class MatrixClient(nio.AsyncClient):
password, device_name or self.default_device_name(), password, device_name or self.default_device_name(),
) )
if order is None and not self.models["accounts"]: saved = await self.backend.saved_accounts.read()
if order is None and not saved.values():
order = 0 order = 0
elif order is None: elif order is None:
order = max( order = max(
account.order account.get("order", i)
for i, account in enumerate(self.models["accounts"].values()) for i, account in enumerate(saved.values())
) + 1 ) + 1
# Get or create account model # Get or create account model
@ -519,10 +522,8 @@ class MatrixClient(nio.AsyncClient):
mentions = mentions, mentions = mentions,
) )
while ( presence = self.models["accounts"][self.user_id].presence
self.models["accounts"][self.user_id].presence == while presence == Presence.State.offline:
Presence.State.offline
):
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
await self._send_message(room_id, content, tx_id) await self._send_message(room_id, content, tx_id)
@ -550,10 +551,8 @@ class MatrixClient(nio.AsyncClient):
async def send_file(self, room_id: str, path: Union[Path, str]) -> None: async def send_file(self, room_id: str, path: Union[Path, str]) -> None:
"""Send a `m.file`, `m.image`, `m.audio` or `m.video` message.""" """Send a `m.file`, `m.image`, `m.audio` or `m.video` message."""
while ( presence = self.models["accounts"][self.user_id].presence
self.models["accounts"][self.user_id].presence == while presence == Presence.State.offline:
Presence.State.offline
):
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
item_uuid = uuid4() item_uuid = uuid4()
@ -1123,10 +1122,8 @@ class MatrixClient(nio.AsyncClient):
"""Set typing notice to the server.""" """Set typing notice to the server."""
# Do not send typing notice if the user is invisible # Do not send typing notice if the user is invisible
if ( presence = self.models["accounts"][self.user_id].presence
self.models["accounts"][self.user_id].presence not in if presence not in [Presence.State.invisible, Presence.State.offline]:
[Presence.State.invisible, Presence.State.offline]
):
await super().room_typing(room_id, typing_state, timeout) await super().room_typing(room_id, typing_state, timeout)
@ -1304,20 +1301,20 @@ class MatrixClient(nio.AsyncClient):
await self._stop() await self._stop()
# Uppdate manually since we may not receive the presence event back # Update manually since we may not receive the presence event back
# in time # in time
account.presence = Presence.State.offline account.presence = Presence.State.offline
account.currently_active = False account.currently_active = False
elif ( elif (
presence != "offline" and account.presence == Presence.State.offline and
account.presence == Presence.State.offline presence != "offline"
): ):
account.connecting = True account.connecting = True
self.start_task = asyncio.ensure_future(self._start()) self.start_task = asyncio.ensure_future(self._start())
if ( if (
presence != "offline" and Presence.State(presence) != account.presence and
Presence.State(presence) != account.presence presence != "offline"
): ):
account.presence = Presence.State("echo_" + presence) account.presence = Presence.State("echo_" + presence)
@ -1689,7 +1686,7 @@ class MatrixClient(nio.AsyncClient):
async def add_member(self, room: nio.MatrixRoom, user_id: str) -> None: async def add_member(self, room: nio.MatrixRoom, user_id: str) -> None:
"""Register/update a room member into our models.""" """Register/update a room member into our models."""
member = room.users[user_id] member = room.users[user_id]
presence = self.backend.presences.get(user_id, Presence()) presence = self.backend.presences.get(user_id, None)
member_item = Member( member_item = Member(
id = user_id, id = user_id,
display_name = room.user_name(user_id) # disambiguated display_name = room.user_name(user_id) # disambiguated
@ -1701,8 +1698,8 @@ class MatrixClient(nio.AsyncClient):
) )
# Associate presence with member, if it exists # Associate presence with member, if it exists
if user_id in self.backend.presences: if presence:
presence.members[room.room_id, user_id] = member_item presence.members[room.room_id] = member_item
# And then update presence fields # And then update presence fields
presence.update_members() presence.update_members()

View File

@ -17,6 +17,11 @@ from .model_item import ModelItem
ZeroDate = datetime.fromtimestamp(0) ZeroDate = datetime.fromtimestamp(0)
OptionalExceptionType = Union[Type[None], Type[Exception]] OptionalExceptionType = Union[Type[None], Type[Exception]]
PresenceOrder: Dict[str, int] = {
"online": 0,
"unavailable": 1,
"offline": 2,
}
class TypeSpecifier(AutoStrEnum): class TypeSpecifier(AutoStrEnum):
@ -28,7 +33,38 @@ class TypeSpecifier(AutoStrEnum):
@dataclass @dataclass
class Presence(): class Presence:
"""Represents a single matrix user presence fields.
It is stored in `Backend.presences`, indexed by user ID. It must only be
instiated when receiving a `PresenceEvent` or registering an `Account`
model.
When receiving a `PresenceEvent`, we get or create a `Presence` object in
`Backend.presences` for the targeted user. If the user is registered in any
room, add its `Member` object to `members`. And finally update every
`Member` presence fields inside `members`.
When a room member is registered, we try to find a `Presence` in
`Backend.presences` for that user ID. If found, add the member to
`members`.
When an Account model is registered, we create a `Presence` in
`Backend.presences` for account ID wether the server supports or not
presence (we cannot really know at this point). And assign that `Account`
to `Account` field.
Attributes:
members: A `{room_id: Member}` dict for storing room members related to
this `Presence`. As each room has its own `Member`s objects, we
have to keep track of their presence fields. `Member`s are indexed
by room ID.
account: `Account` related to this `Presence` (if any). Should only be
assigned when client starts (`MatrixClient._start()`) and
unassigned when client stops (`MatrixClient._start()`).
"""
class State(AutoStrEnum): class State(AutoStrEnum):
offline = auto() # can mean offline, invisible or unknwon offline = auto() # can mean offline, invisible or unknwon
unavailable = auto() unavailable = auto()
@ -40,28 +76,24 @@ class Presence():
echo_invisible = auto() echo_invisible = auto()
def __lt__(self, other: "Presence.State") -> bool: def __lt__(self, other: "Presence.State") -> bool:
order = [ return PresenceOrder[self.value] < PresenceOrder[other.value]
self.online,
self.unavailable,
self.invisible,
self.offline,
]
return (
order.index(self) # type: ignore
) < (
order.index(other) # type: ignore
)
presence: State = State.offline presence: State = State.offline
currently_active: bool = False currently_active: bool = False
last_active_at: datetime = ZeroDate last_active_at: datetime = ZeroDate
status_msg: str = "" status_msg: str = ""
members: Dict[Tuple[str, str], "Member"] = field(default_factory=dict) members: Dict[str, "Member"] = field(default_factory=dict)
account: Optional["Account"] = None account: Optional["Account"] = None
def update_members(self) -> None: def update_members(self) -> None:
"""Update presence fields of every `M̀ember` in `members`.
Currently it is only called when receiving a `PresenceEvent` and when
registering room members.
"""
for member in self.members.values(): for member in self.members.values():
member.presence = self.presence member.presence = self.presence
member.status_msg = self.status_msg member.status_msg = self.status_msg
@ -69,6 +101,8 @@ class Presence():
member.currently_active = self.currently_active member.currently_active = self.currently_active
def update_account(self) -> None: def update_account(self) -> None:
"""Update presence fields of `Account` related to this `Presence`."""
# Do not update if account is changing to invisible. # Do not update if account is changing to invisible.
# When setting presence to invisible, the server will give us a # When setting presence to invisible, the server will give us a
# presence event telling us we are offline, but we do not want to set # presence event telling us we are offline, but we do not want to set

View File

@ -593,13 +593,14 @@ class NioCallbacks:
async def onPresenceEvent(self, ev: nio.PresenceEvent) -> None: async def onPresenceEvent(self, ev: nio.PresenceEvent) -> None:
presence = self.client.backend.presences.get(ev.user_id, Presence()) presence = self.client.backend.presences.get(ev.user_id, Presence())
presence.currently_active = ev.currently_active or False
presence.status_msg = ev.status_msg or "" presence.status_msg = ev.status_msg or ""
presence.presence = Presence.State(ev.presence) if ev.presence\
else Presence.State.offline
presence.last_active_at = ( presence.last_active_at = (
datetime.now() - timedelta(milliseconds=ev.last_active_ago) datetime.now() - timedelta(milliseconds=ev.last_active_ago)
) if ev.last_active_ago else datetime.fromtimestamp(0) ) if ev.last_active_ago else datetime.fromtimestamp(0)
presence.currently_active = ev.currently_active or False
presence.presence = Presence.State(ev.presence) if ev.presence\
else Presence.State.offline
# Add all existing members related to this presence # Add all existing members related to this presence
for room_id in self.models[self.user_id, "rooms"]: for room_id in self.models[self.user_id, "rooms"]:
@ -608,7 +609,7 @@ class NioCallbacks:
) )
if member: if member:
presence.members[room_id, ev.user_id] = member presence.members[room_id] = member
# Update members and accounts # Update members and accounts
presence.update_members() presence.update_members()
@ -624,7 +625,7 @@ class NioCallbacks:
): ):
account = self.models["accounts"][ev.user_id] account = self.models["accounts"][ev.user_id]
# Do not fight back presence # Do not fight back presence from other clients
self.client.backend.clients[ev.user_id]._presence = ev.presence self.client.backend.clients[ev.user_id]._presence = ev.presence
# Servers that send presence events support presence # Servers that send presence events support presence

View File

@ -194,12 +194,7 @@ class Accounts(JSONDataFile):
"device_id": client.device_id, "device_id": client.device_id,
"enabled": True, "enabled": True,
"presence": account.presence.value, "presence": account.presence.value,
"order": account.order,
# Can account.order converge with any other saved value?
"order": account.order if account.order >= 0 else max([
account.get("order", i)
for i, account in enumerate(saved.values())
] or [-1]) + 1,
}, },
}) })

View File

@ -1,7 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Shapes 1.12
HAvatar { HAvatar {
name: displayName || userId.substring(1) // no leading @ name: displayName || userId.substring(1) // no leading @
@ -62,8 +61,10 @@ HAvatar {
opacity: theme.controls.presence.opacity opacity: theme.controls.presence.opacity
z: 100 z: 100
property bool small: window.settings.compactMode property int diameter:
property int diameter: small ? 10 : 15 window.settings.compactMode ?
theme.controls.presence.radius * 2 :
theme.controls.presence.radius * 2.5
sourceComponent: Rectangle { sourceComponent: Rectangle {
width: diameter width: diameter
@ -83,7 +84,7 @@ HAvatar {
theme.controls.presence.offline theme.controls.presence.offline
border.color: theme.controls.presence.border border.color: theme.controls.presence.border
border.width: diameter / 10 border.width: Math.ceil(diameter / 10)
Behavior on color { HColorAnimation {} } Behavior on color { HColorAnimation {} }
Behavior on opacity { HNumberAnimation {} } Behavior on opacity { HNumberAnimation {} }
@ -91,8 +92,8 @@ HAvatar {
HoverHandler { id: presenceHover } HoverHandler { id: presenceHover }
HToolTip { HToolTip {
visible: presenceHover.hovered visible: presenceHover.hovered && ! presence.includes("echo")
text: presence.replace(/^\w/, c => c.toUpperCase()) text: qsTr(presence.replace(/^\w/, c => c.toUpperCase()))
} }
} }
} }

View File

@ -16,7 +16,7 @@ HMenu {
onOpened: statusText.forceActiveFocus() onOpened: statusText.forceActiveFocus()
function setPresence(presence, statusMsg = undefined) { function setPresence(presence, statusMsg=undefined) {
py.callClientCoro(userId, "set_presence", [presence, statusMsg]) py.callClientCoro(userId, "set_presence", [presence, statusMsg])
} }
@ -42,7 +42,7 @@ HMenu {
} }
defaultText: statusMsg defaultText: statusMsg
placeholderText: ! presence ? "Unsupported server" : "" placeholderText: presence ? "" : "Unsupported server"
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -66,7 +66,7 @@ HMenu {
HMenuSeparator { } HMenuSeparator { }
HMenuItem { HMenuItem {
icon.name: "presence" icon.name: "presence-online"
icon.color: theme.controls.presence.online icon.color: theme.controls.presence.online
text: qsTr("Online") text: qsTr("Online")
onTriggered: setPresence("online") onTriggered: setPresence("online")

View File

@ -163,11 +163,11 @@ HTile {
contextMenu: AccountContextMenu { contextMenu: AccountContextMenu {
userId: model.id userId: model.id
statusMsg: model.status_msg
presence: presence:
model.presence_support || model.presence === "offline" ? model.presence_support || model.presence === "offline" ?
model.presence : model.presence :
null null
statusMsg: model.status_msg
} }

View File

@ -185,7 +185,7 @@ HTile {
readonly property ListModel eventModel: readonly property ListModel eventModel:
ModelStore.get(model.for_account, model.id, "events") ModelStore.get(model.for_account, model.id, "events")
// TODO: binding loop // FIXME: binding loop
readonly property QtObject accountModel: readonly property QtObject accountModel:
ModelStore.get("accounts").find(model.for_account) ModelStore.get("accounts").find(model.for_account)

View File

@ -43,26 +43,9 @@ HTile {
} }
TitleRightInfoLabel { TitleRightInfoLabel {
id: lastActiveAt
tile: member tile: member
visible: presenceTimer.running visible: presenceTimer.running
Timer {
id: presenceTimer
running:
! model.currently_active &&
model.last_active_at > new Date(1)
repeat: true
interval:
new Date() - model.last_active_at < 60000 ?
10000 :
60000
triggeredOnStart: true
onTriggered: parent.text = Qt.binding(() =>
utils.formatRelativeTime(
new Date() - model.last_active_at
)
)
}
} }
} }
@ -84,6 +67,20 @@ HTile {
"" ""
) )
} }
Timer {
id: presenceTimer
running:
! model.currently_active &&
model.last_active_at > new Date(1)
repeat: true
interval:
new Date() - model.last_active_at < 60000 ? 10000 : 60000
triggeredOnStart: true
onTriggered: lastActiveAt.text = Qt.binding(() =>
utils.formatRelativeTime(new Date() - model.last_active_at)
)
}
} }
} }

View File

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 647 B

View File

@ -273,6 +273,7 @@ controls:
color offline: hsluv(0, 0, 30, 1) color offline: hsluv(0, 0, 30, 1)
color border: "black" color border: "black"
real opacity: 1.0 real opacity: 1.0
real radius: 6.0 * uiScale
// Specific interface parts // Specific interface parts

View File

@ -279,6 +279,7 @@ controls:
color offline: hsluv(0, 0, 30, 1) color offline: hsluv(0, 0, 30, 1)
color border: "black" color border: "black"
real opacity: 1.0 real opacity: 1.0
real radius: 6.0 * uiScale
// Specific interface parts // Specific interface parts