feat: client-side knock auto-accept via KnockTracker (#5794)
* feat: client-side knock auto-accept via KnockTracker Replace server-side AutoAcceptInviteIfKnocked (removed in synapse-pangea-chat PR #21) with client-side KnockTracker. - Record knocked room IDs in Matrix account data (org.pangea.knocked_rooms) - Auto-join when invite arrives for a previously knocked room - Migrate storage from GetStorage to Matrix account data for cross-device sync and reinstall persistence - Add joining-courses.instructions.md design doc * formatting * centralizes calls to knock-storage related functions --------- Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
parent
54a9a9a500
commit
7f1e8f5ea0
10 changed files with 320 additions and 93 deletions
125
.github/instructions/joining-courses.instructions.md
vendored
Normal file
125
.github/instructions/joining-courses.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
applyTo: "lib/pangea/join_codes/**,lib/pangea/chat_list/**,lib/pangea/course_creation/**,lib/pangea/spaces/**,lib/pages/chat_list/**,lib/utils/url_launcher.dart,lib/config/routes.dart"
|
||||
---
|
||||
|
||||
# Joining Courses — Client Design
|
||||
|
||||
How users join courses (Matrix spaces) through three routes: class link, class code, and knock-accept.
|
||||
|
||||
- **Synapse module cleanup**: [synapse-pangea-chat PR #21](https://github.com/pangeachat/synapse-pangea-chat/pull/21)
|
||||
- **Course plans**: [course-plans.instructions.md](course-plans.instructions.md)
|
||||
|
||||
---
|
||||
|
||||
## Join Routes Overview
|
||||
|
||||
| Route | Entry Point | Mechanism | User Interaction |
|
||||
| ---------------- | -------------------- | ---------------------------------------------------------- | --------------------------------- |
|
||||
| **Class link** | Deep link URL | Extracts class code from URL → knock_with_code → join | None after click (auto-join) |
|
||||
| **Class code** | Manual text input | knock_with_code → join | Enter code only |
|
||||
| **Knock-accept** | "Ask to Join" button | Matrix knock → admin approves → invite → client auto-joins | Knock button; then wait for admin |
|
||||
|
||||
All three routes converge on the user becoming a `Membership.join` member of the course space. Child rooms (announcements, introductions, activity chats) are joined separately afterward.
|
||||
|
||||
---
|
||||
|
||||
## Route 1 — Class Link
|
||||
|
||||
**URL format**: `https://pangea.chat/#/join_with_link?classcode=XYZ`
|
||||
|
||||
1. User clicks link → GoRouter navigates to `/join_with_link`
|
||||
2. Class code is saved to local storage (`SpaceCodeRepo`)
|
||||
3. Redirect to `/home`
|
||||
4. On home load, `joinCachedSpaceCode()` fires the knock_with_code + join flow (same as Route 2)
|
||||
|
||||
**Pre-login persistence**: If not logged in, the code persists on disk. After account creation, `joinCachedSpaceCode()` auto-joins.
|
||||
|
||||
**matrix.to links**: Standard Matrix links (`https://matrix.to/#/...`) go through a separate path. For knock-only rooms, this shows the public room bottom sheet with a code field and "Ask to Join" button.
|
||||
|
||||
---
|
||||
|
||||
## Route 2 — Class Code
|
||||
|
||||
Three UI entry points (onboarding, in-app code page, public room bottom sheet) all converge on `SpaceCodeController.joinSpaceWithCode()`:
|
||||
|
||||
1. Save code as `recentCode` (used for invite dedup in Route 3)
|
||||
2. `POST /_synapse/client/pangea/v1/knock_with_code` — Pangea-custom Synapse endpoint that validates the access code and invites the user
|
||||
3. Response contains `{ roomIds, alreadyJoined, rateLimited }`
|
||||
4. `joinRoomById(spaceId)` → wait for sync → navigate to space
|
||||
|
||||
This is NOT a standard Matrix knock — the Synapse module handles the invite directly.
|
||||
|
||||
---
|
||||
|
||||
## Route 3 — Knock-Accept
|
||||
|
||||
### User knocks
|
||||
|
||||
User finds the course in one of three places:
|
||||
|
||||
- **Public course preview** — linked from `matrix.to` or direct room ID navigation. Shows a "Join" button that becomes a knock if the room's join rule is `knock`.
|
||||
- **Public room bottom sheet** — search results for public rooms. For knock rooms, shows a code field and an "Ask to Join" button side by side. TODO: change to "Knock" for consistency.
|
||||
- **Public room dialog** — a simpler variant of the bottom sheet used in some navigation paths. Button label changes to "Knock" for knock-rule rooms.
|
||||
|
||||
User taps the knock button → standard Matrix `knockRoom()` call → confirmation dialog ("You have knocked") → wait for admin.
|
||||
|
||||
### Admin approves
|
||||
|
||||
Admin notices the knock in one of three places:
|
||||
|
||||
- **Notification badge** on the space icon in the navigation bar. TODO: need to add this.
|
||||
- **Chat list** for that space — the knocking user appears as a pending entry. TODO: make this a bit more attention-grabbing.
|
||||
- **Member list** (room participants page) — knocking users are sorted below joined members, labeled "Knocking."
|
||||
|
||||
Admin taps the knocking user → popup menu shows "Approve" (only visible for `Membership.knock`) → `room.invite(userId)`.
|
||||
|
||||
**Analytics room knocks** follow the same mechanism — admins request access to a student's analytics room by knocking, and the student's client auto-accepts when the invite arrives.
|
||||
|
||||
---
|
||||
|
||||
## Invite Handling
|
||||
|
||||
When an invite arrives via `/sync`, the sync listener routes it by room type:
|
||||
|
||||
| Room type | Action |
|
||||
| ------------------------------ | ------------------------------------------------- |
|
||||
| **Space** | Evaluate priority rules (below) |
|
||||
| **Analytics room** | Auto-join immediately |
|
||||
| **Previously knocked room** | Auto-join immediately |
|
||||
| **Other** | No sync-time action; handled when user taps it |
|
||||
|
||||
### Space invite priority
|
||||
|
||||
When a space invite arrives, the client evaluates in order:
|
||||
|
||||
1. **Child of joined parent** → auto-join (no prompt)
|
||||
2. **Code just inputted** → skip (Route 2 is handling it)
|
||||
3. **Previously knocked** → auto-join (no prompt)
|
||||
4. **Otherwise** → show accept/decline dialog
|
||||
|
||||
### KnockTracker
|
||||
|
||||
The server-side `auto_accept_invite` module was removed because it crashed Synapse ([PR #21](https://github.com/pangeachat/synapse-pangea-chat/pull/21)). Auto-accept now lives client-side via [`KnockTracker`](../../lib/pangea/join_codes/knock_tracker.dart).
|
||||
|
||||
Matrix `/sync` invites use `StrippedStateEvent`, which lacks `unsigned` / `prev_content` — so the client can't tell from the invite alone whether it previously knocked. `KnockTracker` solves this by recording each knocked room ID in Matrix account data (`org.pangea.knocked_rooms`). When an invite arrives for a tracked room, the client auto-joins and clears the record.
|
||||
|
||||
Account data is used so the state survives reinstall, logout, and syncs across devices. Only the user can initiate a knock, so an invite for a tracked room is always a legitimate approval. Applies to both spaces and non-space rooms.
|
||||
|
||||
### All auto-join cases
|
||||
|
||||
Every case where `room.join()` is called without explicit user confirmation:
|
||||
|
||||
| Condition | Trigger |
|
||||
| -------------------------------------------------------- | --------------------------------- |
|
||||
| Navigating to an invited room's chat view | Building the widget |
|
||||
| Invited space is a child of a joined parent | Space invite via sync |
|
||||
| Tapping a left space in the list | User tap |
|
||||
| Analytics room invite | Always auto-joined |
|
||||
| Default chats in a course (announcements, introductions) | Viewing the course |
|
||||
| knock_with_code succeeded | Code entry flow |
|
||||
| User previously knocked on the room | Invite received for a prior knock |
|
||||
|
||||
---
|
||||
|
||||
## Future Work
|
||||
|
||||
|
|
@ -18,6 +18,8 @@ import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart
|
|||
import 'package:fluffychat/pangea/chat_settings/widgets/chat_context_menu_action.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_tracker.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/space_code_repo.dart';
|
||||
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
|
||||
|
|
@ -550,6 +552,10 @@ class ChatListController extends State<ChatList>
|
|||
event.type == EventTypes.RoomCreate &&
|
||||
event.content['type'] == PangeaRoomTypes.analytics,
|
||||
);
|
||||
final hasKnocked = KnockTracker.hasKnocked(
|
||||
Matrix.of(context).client,
|
||||
inviteEntry.key,
|
||||
);
|
||||
|
||||
if (isSpace) {
|
||||
final spaceId = inviteEntry.key;
|
||||
|
|
@ -565,18 +571,20 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
}
|
||||
|
||||
if (isAnalytics) {
|
||||
final analyticsRoom = Matrix.of(
|
||||
if (isAnalytics || hasKnocked) {
|
||||
final room = Matrix.of(
|
||||
context,
|
||||
).client.getRoomById(inviteEntry.key);
|
||||
if (room == null) return;
|
||||
|
||||
try {
|
||||
await analyticsRoom?.join();
|
||||
await room.joinKnockedRoom();
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
m: "Failed to join analytics room",
|
||||
e: err,
|
||||
s: s,
|
||||
data: {"analyticsRoom": analyticsRoom?.id},
|
||||
data: {"roomId": room.id},
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/space_code_repo.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
|
|
@ -73,7 +74,7 @@ Future<void> showInviteDialog(Room room, BuildContext context) async {
|
|||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await room.join();
|
||||
await room.joinKnockedRoom();
|
||||
},
|
||||
exceptionContext: ExceptionContext.joinRoom,
|
||||
);
|
||||
|
|
@ -98,7 +99,7 @@ void chatListHandleSpaceTap(BuildContext context, Room space) {
|
|||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await space.join();
|
||||
await space.joinKnockedRoom();
|
||||
setActiveSpaceAndCloseChat();
|
||||
},
|
||||
);
|
||||
|
|
@ -121,6 +122,8 @@ void chatListHandleSpaceTap(BuildContext context, Room space) {
|
|||
} else if (justInputtedCode != null &&
|
||||
justInputtedCode == space.classCode) {
|
||||
// do nothing
|
||||
} else if (space.hasKnocked) {
|
||||
autoJoin(space);
|
||||
} else {
|
||||
showInviteDialog(space, context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -131,7 +132,7 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
|
|||
await showFutureLoadingDialog<String>(
|
||||
context: context,
|
||||
future: () async =>
|
||||
client.knockRoom(roomAlias ?? chunk!.roomId, via: via),
|
||||
client.knockAndRecordRoom(roomAlias ?? chunk!.roomId, via: via),
|
||||
onSuccess: () => L10n.of(context).knockSpaceSuccess,
|
||||
delay: false,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:fluffychat/pangea/course_plans/course_activities/activity_summar
|
|||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
|
||||
import 'package:fluffychat/pangea/spaces/space_constants.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
|
|
@ -334,96 +335,113 @@ class CourseChatsController extends State<CourseChats>
|
|||
|
||||
void onChatTap(Room room) async {
|
||||
if (room.membership == Membership.invite) {
|
||||
final theme = Theme.of(context);
|
||||
final inviteEvent = room.getState(
|
||||
EventTypes.RoomMember,
|
||||
room.client.userID!,
|
||||
);
|
||||
final matrixLocals = MatrixLocals(L10n.of(context));
|
||||
final action = await showAdaptiveDialog<InviteAction>(
|
||||
barrierDismissible: true,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256),
|
||||
child: Center(
|
||||
if (room.hasKnocked) {
|
||||
if (!mounted) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.joinKnockedRoom();
|
||||
await waitForRoom;
|
||||
},
|
||||
exceptionContext: ExceptionContext.joinRoom,
|
||||
);
|
||||
} else {
|
||||
final theme = Theme.of(context);
|
||||
final inviteEvent = room.getState(
|
||||
EventTypes.RoomMember,
|
||||
room.client.userID!,
|
||||
);
|
||||
final matrixLocals = MatrixLocals(L10n.of(context));
|
||||
final action = await showAdaptiveDialog<InviteAction>(
|
||||
barrierDismissible: true,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256),
|
||||
child: Center(
|
||||
child: Text(
|
||||
room.getLocalizedDisplayname(matrixLocals),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
|
||||
child: Text(
|
||||
room.getLocalizedDisplayname(matrixLocals),
|
||||
inviteEvent == null
|
||||
? L10n.of(context).inviteForMe
|
||||
: inviteEvent.content.tryGet<String>('reason') ??
|
||||
L10n.of(context).youInvitedBy(
|
||||
room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
inviteEvent.senderId,
|
||||
)
|
||||
.calcDisplayname(i18n: matrixLocals),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
|
||||
child: Text(
|
||||
inviteEvent == null
|
||||
? L10n.of(context).inviteForMe
|
||||
: inviteEvent.content.tryGet<String>('reason') ??
|
||||
L10n.of(context).youInvitedBy(
|
||||
room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
inviteEvent.senderId,
|
||||
)
|
||||
.calcDisplayname(i18n: matrixLocals),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.accept),
|
||||
bigButtons: true,
|
||||
child: Text(L10n.of(context).accept),
|
||||
),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.decline),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).decline,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
actions: [
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.accept),
|
||||
bigButtons: true,
|
||||
child: Text(L10n.of(context).accept),
|
||||
),
|
||||
),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.block),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).block,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(InviteAction.decline),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).decline,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
switch (action) {
|
||||
case null:
|
||||
return;
|
||||
case InviteAction.accept:
|
||||
break;
|
||||
case InviteAction.decline:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.leave(),
|
||||
);
|
||||
return;
|
||||
case InviteAction.block:
|
||||
final userId = inviteEvent?.senderId;
|
||||
context.go('/rooms/settings/security/ignorelist', extra: userId);
|
||||
return;
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.block),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).block,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
switch (action) {
|
||||
case null:
|
||||
return;
|
||||
case InviteAction.accept:
|
||||
break;
|
||||
case InviteAction.decline:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.leave(),
|
||||
);
|
||||
return;
|
||||
case InviteAction.block:
|
||||
final userId = inviteEvent?.senderId;
|
||||
context.go('/rooms/settings/security/ignorelist', extra: userId);
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
},
|
||||
exceptionContext: ExceptionContext.joinRoom,
|
||||
);
|
||||
if (joinResult.error != null) return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
},
|
||||
exceptionContext: ExceptionContext.joinRoom,
|
||||
);
|
||||
if (joinResult.error != null) return;
|
||||
}
|
||||
|
||||
if (room.membership == Membership.ban) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/course_creation/public_course_preview_view.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
|
@ -140,7 +141,7 @@ class PublicCoursePreviewController extends State<PublicCoursePreview>
|
|||
String roomId;
|
||||
try {
|
||||
roomId = knock
|
||||
? await client.knockRoom(widget.roomID!)
|
||||
? await client.knockAndRecordRoom(widget.roomID!)
|
||||
: await client.joinRoom(widget.roomID!);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(e: e, s: s, data: {'roomID': widget.roomID});
|
||||
|
|
|
|||
24
lib/pangea/join_codes/knock_room_extension.dart
Normal file
24
lib/pangea/join_codes/knock_room_extension.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/join_codes/knock_tracker.dart';
|
||||
|
||||
extension KnockRoomExtension on Room {
|
||||
bool get hasKnocked => KnockTracker.hasKnocked(client, id);
|
||||
|
||||
Future<void> joinKnockedRoom() async {
|
||||
await join();
|
||||
await KnockTracker.clearKnock(client, id);
|
||||
}
|
||||
}
|
||||
|
||||
extension KnockClientExtension on Client {
|
||||
Future<String> knockAndRecordRoom(
|
||||
String roomIdOrAlias, {
|
||||
List<String>? via,
|
||||
String? reason,
|
||||
}) async {
|
||||
final resp = await knockRoom(roomIdOrAlias, via: via, reason: reason);
|
||||
await KnockTracker.recordKnock(this, roomIdOrAlias);
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
45
lib/pangea/join_codes/knock_tracker.dart
Normal file
45
lib/pangea/join_codes/knock_tracker.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Tracks room IDs the user has knocked on so the client can auto-accept
|
||||
/// invites for previously knocked rooms.
|
||||
///
|
||||
/// Stored in Matrix account data under [_accountDataKey] so the state
|
||||
/// survives reinstall, logout, and syncs across devices.
|
||||
class KnockTracker {
|
||||
static const String _accountDataKey = 'org.pangea.knocked_rooms';
|
||||
static const String _roomIdsField = 'room_ids';
|
||||
|
||||
static Future<void> recordKnock(Client client, String roomId) async {
|
||||
final ids = _getKnockedRoomIds(client);
|
||||
if (!ids.contains(roomId)) {
|
||||
ids.add(roomId);
|
||||
await _writeIds(client, ids);
|
||||
}
|
||||
}
|
||||
|
||||
static bool hasKnocked(Client client, String roomId) {
|
||||
return _getKnockedRoomIds(client).contains(roomId);
|
||||
}
|
||||
|
||||
static Future<void> clearKnock(Client client, String roomId) async {
|
||||
final ids = _getKnockedRoomIds(client);
|
||||
if (ids.remove(roomId)) {
|
||||
await _writeIds(client, ids);
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> _getKnockedRoomIds(Client client) {
|
||||
final data = client.accountData[_accountDataKey];
|
||||
final list = data?.content[_roomIdsField];
|
||||
if (list is List) {
|
||||
return list.cast<String>().toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<void> _writeIds(Client client, List<String> ids) async {
|
||||
await client.setAccountData(client.userID!, _accountDataKey, {
|
||||
_roomIdsField: ids,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/analytics_misc/saved_analytics_extension.dart'
|
|||
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||
import 'package:fluffychat/pangea/languages/p_language_store.dart';
|
||||
import 'package:fluffychat/pangea/space_analytics/analytics_download_model.dart';
|
||||
|
|
@ -285,7 +286,7 @@ class SpaceAnalyticsState extends State<SpaceAnalytics> {
|
|||
try {
|
||||
final roomId = _analyticsRoomIdOfUser(user);
|
||||
if (roomId == null) return;
|
||||
await Matrix.of(context).client.knockRoom(
|
||||
await Matrix.of(context).client.knockAndRecordRoom(
|
||||
roomId,
|
||||
via: room?.spaceChildren
|
||||
.firstWhereOrNull((child) => child.roomId == roomId)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/join_codes/knock_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import '../../config/themes.dart';
|
||||
import '../../utils/url_launcher.dart';
|
||||
|
|
@ -36,7 +37,7 @@ class PublicRoomDialog extends StatelessWidget {
|
|||
return chunk.roomId;
|
||||
}
|
||||
final roomId = chunk != null && knock
|
||||
? await client.knockRoom(chunk.roomId, via: via)
|
||||
? await client.knockAndRecordRoom(chunk.roomId, via: via)
|
||||
: await client.joinRoom(roomAlias ?? chunk!.roomId, via: via);
|
||||
|
||||
if (!knock && client.getRoomById(roomId) == null) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue