Make offline presence to stop sync

Setting the presence of an account to offline
will make the client to end sync task and will
prevent messages from being sent.

Setting it to online again or any other presence
will start sync task again.

Left:
- Local echo to presence change
- UI Control to affect all members presence
- Block more requests to be sent (e.g. member
  actions)
This commit is contained in:
vslg 2020-07-09 17:06:14 -03:00 committed by miruka
parent 3fa35b88c9
commit a3c9ac20c6
12 changed files with 231 additions and 109 deletions

View File

@ -132,7 +132,7 @@ class Backend:
)
try:
await client.login(password)
await client.login(password, order=order)
except MatrixError:
await client.close()
raise
@ -142,22 +142,7 @@ class Backend:
await client.logout()
return client.user_id
if order is None and not self.models["accounts"]:
order = 0
elif order is None:
order = max(
account.order
for i, account in enumerate(self.models["accounts"].values())
) + 1
account = Account(client.user_id, order)
self.clients[client.user_id] = client
self.models["accounts"][client.user_id] = account
# Get or create presence for account
presence = self.presences.setdefault(client.user_id, Presence())
presence.members["account", client.user_id] = account
return client.user_id
@ -168,7 +153,6 @@ class Backend:
token: str,
device_id: str,
homeserver: str = "https://matrix.org",
order: int = -1,
state: str = "online",
) -> None:
"""Create and register a `MatrixClient` with known account details."""
@ -178,14 +162,7 @@ class Backend:
user=user_id, homeserver=homeserver, device_id=device_id,
)
account = Account(user_id, order)
self.clients[user_id] = client
self.models["accounts"][user_id] = account
# Get or create presence for account
presence = self.presences.setdefault(user_id, Presence())
presence.members["account", user_id] = account
await client.resume(user_id, token, device_id, state)
@ -194,14 +171,19 @@ class Backend:
"""Call `resume_client` for all saved accounts in user config."""
async def resume(user_id: str, info: Dict[str, Any]) -> str:
# Get or create account model
self.models["accounts"].setdefault(
user_id, Account(user_id, info.get("order", -1)),
)
await self.resume_client(
user_id = user_id,
token = info["token"],
device_id = info["device_id"],
homeserver = info["homeserver"],
order = info.get("order", -1),
state = info.get("presence", "online"),
)
return user_id
return await asyncio.gather(*(

View File

@ -40,7 +40,7 @@ from .errors import (
from .html_markdown import HTML_PROCESSOR as HTML
from .media_cache import Media, Thumbnail
from .models.items import (
Event, Member, Presence, Room, Upload, UploadStatus, ZeroDate,
Account, Event, Member, Presence, Room, Upload, UploadStatus, ZeroDate,
)
from .models.model_store import ModelStore
from .nio_callbacks import NioCallbacks
@ -231,13 +231,35 @@ class MatrixClient(nio.AsyncClient):
return f"{__display_name__} on {os_name} {os_ver}".rstrip()
async def login(self, password: str, device_name: str = "") -> None:
async def login(
self, password: str,
device_name: str = "",
order: Optional[int] = None,
) -> None:
"""Login to the server using the account's password."""
await super().login(
password, device_name or self.default_device_name(),
)
if order is None and not self.models["accounts"]:
order = 0
elif order is None:
order = max(
account.order
for i, account in enumerate(self.models["accounts"].values())
) + 1
# Get or create account model
# We need to create account model in here, because _start() needs it
account = self.models["accounts"].setdefault(
self.user_id, Account(self.user_id, order),
)
# TODO: set presence on login
self._presence = "online"
account.presence = Presence.State.online
account.connecting = True
self.start_task = asyncio.ensure_future(self._start())
@ -251,32 +273,21 @@ class MatrixClient(nio.AsyncClient):
"""Login to the server using an existing access token."""
response = nio.LoginResponse(user_id, device_id, token)
account = self.models["accounts"][user_id]
await self.receive_response(response)
self._presence = "offline" if state == "invisible" else state
self.start_task = asyncio.ensure_future(self._start())
account.presence = Presence.State(state)
if state == "invisible":
self.models["accounts"][self.user_id].presence = \
Presence.State.invisible
if state != "offline":
account.connecting = True
self.start_task = asyncio.ensure_future(self._start())
async def logout(self) -> None:
"""Logout from the server. This will delete the device."""
tasks = (
self.profile_task,
self.sync_task,
self.server_config_task,
self.start_task,
)
for task in tasks:
if task:
task.cancel()
with suppress(asyncio.CancelledError):
await task
await self._stop()
await super().logout()
await self.close()
@ -300,8 +311,6 @@ class MatrixClient(nio.AsyncClient):
if future.cancelled(): # Account logged out
return
account = self.models["accounts"][self.user_id]
try:
account.max_upload_size = future.result()
except Exception:
@ -316,6 +325,13 @@ class MatrixClient(nio.AsyncClient):
on_server_config_response,
)
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)
self.profile_task = asyncio.ensure_future(self.update_own_profile())
self.server_config_task = asyncio.ensure_future(
@ -362,6 +378,31 @@ class MatrixClient(nio.AsyncClient):
await asyncio.sleep(5)
async def _stop(self) -> None:
"""Stop client tasks. Will prevent client to receive further events."""
tasks = (
self.profile_task,
self.sync_task,
self.server_config_task,
self.start_task,
)
for task in tasks:
if task:
task.cancel()
with suppress(asyncio.CancelledError):
await task
self.first_sync_done.clear()
# Remove account model from presence update
presence = self.backend.presences.get(self.user_id, None)
if presence:
presence.members.pop(("account", self.user_id), None)
async def update_own_profile(self) -> None:
"""Fetch our profile from server and Update our model `Account`."""
@ -478,6 +519,12 @@ class MatrixClient(nio.AsyncClient):
mentions = mentions,
)
while (
self.models["accounts"][self.user_id].presence ==
Presence.State.offline
):
await asyncio.sleep(0.2)
await self._send_message(room_id, content, tx_id)
@ -503,6 +550,12 @@ class MatrixClient(nio.AsyncClient):
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."""
while (
self.models["accounts"][self.user_id].presence ==
Presence.State.offline
):
await asyncio.sleep(0.2)
item_uuid = uuid4()
try:
@ -1070,8 +1123,10 @@ class MatrixClient(nio.AsyncClient):
"""Set typing notice to the server."""
# Do not send typing notice if the user is invisible
if self.models["accounts"][self.user_id].presence != \
Presence.State.invisible:
if (
self.models["accounts"][self.user_id].presence not in
[Presence.State.invisible, Presence.State.offline]
):
await super().room_typing(room_id, typing_state, timeout)
@ -1237,21 +1292,46 @@ class MatrixClient(nio.AsyncClient):
) -> None:
"""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
)
if presence == "offline":
# Do not do anything if account is offline and setting to offline
if account.presence == Presence.State.offline:
return
await self._stop()
# Uppdate manually since we may not receive the presence event back
# in time
account.presence = Presence.State.offline
account.currently_active = False
elif (
presence != "offline" and
account.presence == Presence.State.offline
):
account.connecting = True
self.start_task = asyncio.ensure_future(self._start())
# Assign invisible on model in here, because server will tell us we are
# offline
if presence == "invisible":
account.presence = Presence.State.invisible
if not account.presence_support:
account.presence = Presence.State(presence)
await self.backend.saved_accounts.update(
self.user_id, presence=presence,
)
await super().set_presence(
"offline" if presence == "invisible" else presence,
status_msg,
)
# Assign invisible on model in here, because server will tell us we are
# offline
if presence == "invisible":
self.models["accounts"][self.user_id].presence = \
Presence.State.invisible
async def import_keys(self, infile: str, passphrase: str) -> None:
"""Import decryption keys from a file, then retry decrypting events."""

View File

@ -56,27 +56,33 @@ class Presence():
last_active_ago: int = -1
status_msg: str = ""
members: Dict[Tuple[str, str], Union["Member", "Account"]] = \
field(default_factory=dict)
members: Dict[Tuple[str, str], "Member"] = field(default_factory=dict)
account: Optional["Account"] = None
def update_members(self):
def update_members(self) -> None:
for member in self.members.values():
# Do not update if member is changing to invisible
# Because when setting invisible presence will give us presence
# event telling us we are offline, we do not want to set member
# presence to offline.
if (
member.presence == self.State.invisible
) and (
self.presence == self.State.offline
):
continue
member.presence = self.presence
member.status_msg = self.status_msg
member.last_active_ago = self.last_active_ago
member.currently_active = self.currently_active
def update_account(self) -> None:
# Do not update if account is changing to invisible.
# 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
# account presence to offline.
if (
not self.account or
self.account.presence == self.State.invisible and
self.presence == self.State.offline
):
return
self.account.presence = self.presence
self.account.status_msg = self.status_msg
self.account.last_active_ago = self.last_active_ago
self.account.currently_active = self.currently_active
@dataclass
class Account(ModelItem):
@ -88,7 +94,7 @@ class Account(ModelItem):
avatar_url: str = ""
max_upload_size: int = 0
profile_updated: datetime = ZeroDate
first_sync_done: bool = False
connecting: bool = False
total_unread: int = 0
total_highlights: int = 0
local_unreads: bool = False
@ -220,7 +226,7 @@ class AccountOrRoom(Account, Room):
@dataclass
class Member(Presence, ModelItem):
class Member(ModelItem):
"""A member in a matrix room."""
id: str = field()
@ -234,6 +240,11 @@ class Member(Presence, ModelItem):
last_read_event: str = ""
last_read_at: datetime = ZeroDate
presence: Presence.State = Presence.State.offline
currently_active: bool = False
last_active_ago: int = -1
status_msg: str = ""
def __lt__(self, other: "Member") -> bool:
"""Sort by presence, power level, then by display name/user ID."""

View File

@ -106,7 +106,7 @@ class NioCallbacks:
self.client.first_sync_date = datetime.now()
account = self.models["accounts"][self.user_id]
account.first_sync_done = True
account.connecting = False
async def onKeysQueryResponse(self, resp: nio.KeysQueryResponse) -> None:
@ -612,11 +612,21 @@ class NioCallbacks:
presence.update_members()
# Check if presence event is ours
if ev.user_id in self.models["accounts"]:
if (
ev.user_id in self.models["accounts"] and
presence.presence != Presence.State.offline
):
account = self.models["accounts"][ev.user_id]
# Servers that send presence events support presence
self.models["accounts"][ev.user_id].presence_support = True
account.presence_support = True
# Save the presence for the next resume
await self.client.backend.saved_accounts.add(ev.user_id)
await self.client.backend.saved_accounts.update(
user_id = ev.user_id,
presence = presence.presence.value,
)
presence.update_account()
self.client.backend.presences[ev.user_id] = presence

View File

@ -184,7 +184,7 @@ class Accounts(JSONDataFile):
client = self.backend.clients[user_id]
saved = await self.read()
presence = self.backend.models["accounts"][user_id].presence.value
account = self.backend.models["accounts"][user_id]
await self.write({
**saved,
@ -193,8 +193,10 @@ class Accounts(JSONDataFile):
"token": client.access_token,
"device_id": client.device_id,
"enabled": True,
"presence": presence or "online",
"order": max([
"presence": account.presence.value,
# 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,
@ -202,6 +204,24 @@ class Accounts(JSONDataFile):
})
async def update(
self,
user_id: str,
enabled: Optional[str] = None,
presence: Optional[str] = None,
order: Optional[int] = None,
) -> None:
"""Update existing account in the config and write to disk."""
saved = await self.read()
saved[user_id]["enabled"] = enabled or saved[user_id]["enabled"]
saved[user_id]["presence"] = presence or saved[user_id]["presence"]
saved[user_id]["order"] = order or saved[user_id]["order"]
await self.write({**saved})
async def delete(self, user_id: str) -> None:
"""Delete an account from the config and write it on disk."""

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Shapes 1.15
import QtQuick.Shapes 1.12
HAvatar {
name: displayName || userId.substring(1) // no leading @

View File

@ -12,18 +12,16 @@ HMenu {
property string userId
property string presence
property string statusMsg
property bool firstSyncDone
onOpened: statusText.forceActiveFocus()
function setPresence(presence, statusMsg = null) {
function setPresence(presence, statusMsg = undefined) {
py.callClientCoro(userId, "set_presence", [presence, statusMsg])
}
HMenuItem {
enabled: firstSyncDone
icon.name: "presence"
icon.color: theme.controls.presence.online
text: qsTr("Online")
@ -31,8 +29,7 @@ HMenu {
}
HMenuItem {
visible: presence
enabled: firstSyncDone
enabled: presence
icon.name: "presence-busy"
icon.color: theme.controls.presence.unavailable
text: qsTr("Unavailable")
@ -40,7 +37,6 @@ HMenu {
}
HMenuItem {
enabled: firstSyncDone
icon.name: "presence-offline"
icon.color: theme.controls.presence.offline
text: qsTr("Offline")
@ -48,8 +44,7 @@ HMenu {
}
HMenuItem {
visible: presence
enabled: firstSyncDone
enabled: presence
icon.name: "presence-invisible"
icon.color: theme.controls.presence.offline
text: qsTr("Invisible")
@ -60,8 +55,7 @@ HMenu {
HLabeledItem {
id: statusMsgLabel
visible: presence
enabled: firstSyncDone
enabled: presence && presence !== "offline"
width: parent.width
height: visible ? implicitHeight : 0
label.text: qsTr("Status message:")
@ -80,12 +74,14 @@ HMenu {
}
defaultText: statusMsg
placeholderText: ! presence ? "Unsupported server" : ""
Layout.fillWidth: true
}
HButton {
id: button
visible: presence
icon.name: "apply"
icon.color: theme.colors.positiveBackground

View File

@ -13,7 +13,12 @@ HTile {
tile: account
spacing: 0
opacity:
collapsed ? theme.mainPane.listView.account.collapsedOpacity : 1
collapsed ?
theme.mainPane.listView.account.collapsedOpacity :
model.presence == "offline" ?
theme.mainPane.listView.offlineOpacity :
1
Behavior on opacity { HNumberAnimation {} }
@ -25,14 +30,14 @@ HTile {
radius: theme.mainPane.listView.account.avatarRadius
compact: account.compact
presence: model.presence
presence: model.presence_support ? model.presence : ""
Layout.alignment: Qt.AlignCenter
HLoader {
anchors.fill: parent
z: 9998
opacity: model.first_sync_done ? 0 : 1
opacity: model.connecting ? 1 : 0
active: opacity > 0
sourceComponent: Rectangle {
@ -158,11 +163,11 @@ HTile {
contextMenu: AccountContextMenu {
userId: model.id
presence: model.presence_support ? model.presence : null
presence:
model.presence_support || model.presence === "offline" ?
model.presence :
null
statusMsg: model.status_msg
// Gray out buttons before first sync
firstSyncDone: model.first_sync_done
}

View File

@ -15,7 +15,13 @@ HTile {
contentItem: ContentRow {
tile: room
opacity: model.left ? theme.mainPane.listView.room.leftRoomOpacity : 1
opacity:
accountModel.presence === "offline" ?
theme.mainPane.listView.offlineOpacity :
model.left ?
theme.mainPane.listView.room.leftRoomOpacity :
1
Behavior on opacity { HNumberAnimation {} }
@ -179,6 +185,10 @@ HTile {
readonly property ListModel eventModel:
ModelStore.get(model.for_account, model.id, "events")
// TODO: binding loop
readonly property QtObject accountModel:
ModelStore.get("accounts").find(model.for_account)
readonly property QtObject lastEvent:
eventModel.count > 0 ? eventModel.get(0) : null
}

View File

@ -57,6 +57,10 @@ MultiviewPane {
}
readonly property QtObject accountModel:
ModelStore.get("accounts").find(chat.roomInfo.for_account)
function toggleFocus() {
if (roomPane.activeFocus) {
if (roomPane.collapse) roomPane.close()
@ -78,7 +82,9 @@ MultiviewPane {
}
MemberView {}
SettingsView {}
SettingsView {
enabled: accountModel.presence !== "offline"
}
HShortcut {
sequences: window.settings.keys.toggleFocusRoomPane

View File

@ -317,6 +317,7 @@ mainPane:
listView:
color background: colors.mediumBackground
real offlineOpacity: 0.5
account:
real collapsedOpacity: 0.3

View File

@ -326,6 +326,7 @@ mainPane:
listView:
color background: colors.mediumBackground
real offlineOpacity: 0.5
account:
real collapsedOpacity: 0.3