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:
parent
43df8fd60b
commit
5abdc1f779
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 647 B |
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user