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:
		| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	