Presence fixes and improvements

- Allow using invisible mode on servers not supporting presence, to
  make use of the prevention of sending typing notifications and read
  marker updates

- For servers not supporting presence, display the account's presence
  orb in the left pane with half opacity

- Indicate in the presence orb tooltip when the presence we set hasn't
  yet been noticed by the server/the server doesn't support presence

- When reconnecting after being offline, if the server doesn't indicate
  we have a status message [set from another client], restore any
  previous message we had set in the current client

- Show our status message striked out when we're invisible or offline to
  indicate that it isn't being broadcasted

- Some code style cleanups

- Try to handle cases where we set a presence, but receive a new
  presence event for our account before the server takes notice of that
  new presence we want, which probably resulted in the "account keeps
  switching between online and unavailable every few sec" glitch
This commit is contained in:
miruka 2021-04-15 11:05:15 -04:00
parent 909756cff7
commit 18f742966e
7 changed files with 95 additions and 90 deletions

View File

@ -384,15 +384,14 @@ class Backend:
account = self.models["accounts"][client.user_id]
if room:
room.set_fields(
unreads = 0,
highlights = 0,
local_unreads = False,
)
room.set_fields(unreads=0, highlights=0, local_unreads=False)
await client.update_account_unread_counts()
# Only update server markers if the account is not invisible
if account.presence != Presence.State.invisible:
if account.presence not in [
Presence.State.echo_invisible,
Presence.State.invisible,
Presence.State.offline,
]:
await client.update_receipt_marker(room_id, event_id)
await asyncio.gather(*[update(c) for c in self.clients.values()])

View File

@ -306,7 +306,7 @@ class MatrixClient(nio.AsyncClient):
)
# TODO: be able to set presence before logging in
item.set_fields(presence=Presence.State.online, connecting=True)
item.set_fields(presence=Presence.State.echo_online, connecting=True)
self._presence = "online"
self.start_task = asyncio.ensure_future(self._start())
@ -326,6 +326,9 @@ class MatrixClient(nio.AsyncClient):
account = self.models["accounts"][user_id]
self._presence = "offline" if state == "invisible" else state
if state != "offline":
state = f"echo_{state}"
account.set_fields(
presence=Presence.State(state), status_msg=status_msg,
)
@ -385,7 +388,6 @@ class MatrixClient(nio.AsyncClient):
account = self.models["accounts"][self.user_id]
# Get or create presence for account
presence = self.backend.presences.setdefault(self.user_id, Presence())
presence.account = account
presence.presence = Presence.State(self._presence)
@ -518,10 +520,10 @@ class MatrixClient(nio.AsyncClient):
async def pause_while_offline(self) -> None:
"""Block until our account is online."""
while (
self.models["accounts"][self.user_id].presence ==
Presence.State.offline
):
account = self.models["accounts"][self.user_id]
while account.presence == Presence.State.offline:
await asyncio.sleep(0.2)
@ -1382,9 +1384,11 @@ class MatrixClient(nio.AsyncClient):
):
return
presence = self.models["accounts"][self.user_id].presence
if presence not in [Presence.State.invisible, Presence.State.offline]:
if self.models["accounts"][self.user_id].presence not in [
Presence.State.echo_invisible,
Presence.State.invisible,
Presence.State.offline,
]:
await super().room_typing(room_id, typing_state, timeout)
@ -1590,59 +1594,60 @@ class MatrixClient(nio.AsyncClient):
"""Set presence state for this account."""
account = self.models["accounts"][self.user_id]
status_msg = status_msg if status_msg is not None else (
self.models["accounts"][self.user_id].status_msg
)
set_status_msg = True
call_presence_api = True
new_presence = presence
for_server = "offline" if presence == "invisible" else presence
if presence == "offline":
# Do not do anything if account is offline and setting to offline
if status_msg is None:
status_msg = account.status_msg
# Starting/stopping client if current/new presence is offline
if new_presence == "offline":
if account.presence == Presence.State.offline:
return
await self._stop()
# Update manually since we may not receive the presence event back
# in time
# We stop syncing, so update the account manually
account.set_fields(
presence = Presence.State.offline,
status_msg = "",
currently_active = False,
)
elif (
account.presence == Presence.State.offline and
presence != "offline"
):
# In this case we will not run super().set_presence()
set_status_msg = False
elif account.presence == Presence.State.offline:
# We might receive a recent status_msg set from another client on
# startup, so don't try to set a new one immediatly.
# Presence though will be sent on first sync.
self._presence = for_server
call_presence_api = False
account.connecting = True
self.start_task = asyncio.ensure_future(self._start())
self._presence = "offline" if presence == "invisible" else presence
# Update our account model item's presence
if (
Presence.State(presence) != account.presence and
presence != "offline"
new_presence != "offline"
):
account.presence = Presence.State("echo_" + presence)
account.presence = Presence.State("echo_" + new_presence)
if not account.presence_support:
account.presence = Presence.State(presence)
# Saving new details in accounts.json
if save:
account.save_presence = True
await self.backend.saved_accounts.set(
self.user_id, presence=presence, status_msg=status_msg,
self.user_id, presence=new_presence, status_msg=status_msg,
)
else:
account.save_presence = False
if set_status_msg:
account.status_msg = status_msg
# Update our presence/status on the server
await super().set_presence(
"offline" if presence == "invisible" else presence,
status_msg,
)
if call_presence_api:
account.status_msg = status_msg
await super().set_presence(for_server, status_msg)
async def import_keys(self, infile: str, passphrase: str) -> None:

View File

@ -869,63 +869,60 @@ class NioCallbacks:
# Presence event callbacks
async def onPresenceEvent(self, ev: nio.PresenceEvent) -> None:
# Servers that send presence events support presence
self.models["accounts"][self.client.user_id].presence_support = True
account = self.models["accounts"].get(ev.user_id)
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.last_active_at = (
datetime.now() - timedelta(milliseconds=ev.last_active_ago)
) if ev.last_active_ago else datetime.fromtimestamp(0)
presence.presence = \
Presence.State(ev.presence) if ev.presence else \
Presence.State.offline
if ev.last_active_ago:
presence.last_active_at = datetime.now() - timedelta(
milliseconds=ev.last_active_ago,
)
else:
presence.last_active_at = datetime.fromtimestamp(0)
if ev.presence:
presence.presence = Presence.State(ev.presence)
else:
presence.presence = Presence.State.offline
# Add all existing members related to this presence
for room_id in self.models[self.user_id, "rooms"]:
member = self.models[self.user_id, room_id, "members"].get(
ev.user_id,
)
members = self.models[self.user_id, room_id, "members"]
if member:
presence.members[room_id] = member
if ev.user_id in members:
presence.members[room_id] = members[ev.user_id]
# Update members and accounts
presence.update_members()
# Check if presence event is ours
# If presence event represents a change for one of our account
if account and account.presence != Presence.State.offline:
# Ignore cases where we send a new presence to the server, but it
# returns an older state that doesn't match due to lag
if (
ev.user_id in self.models["accounts"] and
self.models["accounts"][ev.user_id].presence !=
Presence.State.offline and
not (
presence.presence == Presence.State.offline and
self.models["accounts"][ev.user_id].presence !=
Presence.State.echo_invisible
)
account.presence == Presence.State.echo_invisible and
ev.presence != Presence.State.offline.value
) or (
account.presence == Presence.State.echo_unavailable and
ev.presence != Presence.State.unavailable.value
) or (
account.presence == Presence.State.echo_online and
ev.presence != Presence.State.online.value
):
account = self.models["accounts"][ev.user_id]
# Set status_msg if none is set on the server and we have one
if (
not presence.status_msg and
account.status_msg and
ev.user_id in self.client.backend.clients and
account.presence != Presence.State.echo_invisible and
presence.presence == Presence.State.offline
):
asyncio.ensure_future(
self.client.backend.clients[ev.user_id].set_presence(
presence.presence.value,
account.status_msg,
),
)
return
# Do not fight back presence from other clients
self.client.backend.clients[ev.user_id]._presence = ev.presence
# Servers that send presence events support presence
account.presence_support = True
# Restore status msg lost from server due to e.g. getting offline
if not ev.status_msg and account.status_msg:
await self.client.backend.clients[ev.user_id].set_presence(
ev.presence, account.status_msg,
)
# Save the presence for the next resume
if account.save_presence:
@ -943,5 +940,5 @@ class NioCallbacks:
)
presence.update_account()
else:
self.client.backend.presences[ev.user_id] = presence

View File

@ -77,7 +77,7 @@ class Presence:
def update_members(self) -> None:
"""Update presence fields of every `M̀ember` in `members`.
"""Update presence fields of every `Member` in `members`.
Currently it is only called when receiving a `PresenceEvent` and when
registering room members.

View File

@ -35,10 +35,11 @@ Rectangle {
HToolTip {
visible: presenceHover.hovered
text:
text: qsTr("%1 (%2)").arg(
presence.includes("online") ? qsTr("Online") :
presence.includes("unavailable") ? qsTr("Unavailable") :
presence.includes("invisible") ? qsTr("Invisible") :
qsTr("Offline")
).arg("unknown to server")
}
}

View File

@ -87,7 +87,6 @@ HMenu {
}
HMenuItem {
enabled: presence
icon.name: "presence-invisible"
icon.color: theme.controls.presence.offline
text: qsTr("Invisible")

View File

@ -124,6 +124,10 @@ HTile {
tile: account
text: utils.escapeHtml(model.status_msg.trim())
visible: model.status_msg.trim()
font.strikeout:
! model.presence_support ||
model.presence.includes("offline") ||
model.presence.includes("invisible")
Layout.leftMargin: theme.spacing
}