feat: cartões de publicação em canais e corrreções de salas arquivadas
- Canais: publicações viram cartões de largura total mantendo todas as funções do balão (reações, resposta, editar, excluir, comentários em thread). Composer de publicação fica restrito a admins no feed; membros comentam via tela de thread. Canais novos usam events_default 0 para permitir comentários e reações. - Ao sair/excluir uma sala, a cópia local é removida do banco imediatamente; salas já deixadas no servidor não apresentam mais erro ao excluir de novo. - Classificação mural/canal carregada direto do sync (room_type em importantStateEvents + postLoad) sem precisar abrir a sala. - Diálogos com raio e padding reduzidos (dialogTheme, text input e ok/cancel). - Ícones do app regenerados (logo_background) e dependência linkify.
This commit is contained in:
@@ -14,6 +14,7 @@ import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
import 'package:tatchat/utils/error_reporter.dart';
|
||||
import 'package:tatchat/utils/localized_exception_extension.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/room_extension.dart';
|
||||
import 'package:tatchat/utils/platform_infos.dart';
|
||||
import 'package:tatchat/utils/show_scaffold_dialog.dart';
|
||||
import 'package:tatchat/utils/show_update_snackbar.dart';
|
||||
@@ -402,6 +403,7 @@ class ChatListController extends State<ChatList>
|
||||
}
|
||||
|
||||
StreamSubscription? _onRoomTagUpdate;
|
||||
StreamSubscription? _backfillSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -411,6 +413,8 @@ class ChatListController extends State<ChatList>
|
||||
|
||||
scrollController.addListener(_onScroll);
|
||||
_waitForFirstSync();
|
||||
_backfillSubscription =
|
||||
_clientStream.stream.listen((_) => backfillRoomTypeMarkers());
|
||||
if (PlatformInfos.isMobile) {
|
||||
_callEventSubscription = FlutterCallkitIncoming.onEvent.listen(
|
||||
_onCallEvent,
|
||||
@@ -474,6 +478,7 @@ class ChatListController extends State<ChatList>
|
||||
_intentFileStreamSubscription?.cancel();
|
||||
_callEventSubscription?.cancel();
|
||||
_onRoomTagUpdate?.cancel();
|
||||
_backfillSubscription?.cancel();
|
||||
scrollController.removeListener(_onScroll);
|
||||
searchController.dispose();
|
||||
searchFocusNode.dispose();
|
||||
@@ -775,7 +780,10 @@ class ChatListController extends State<ChatList>
|
||||
if (confirmed == OkCancelResult.cancel) return;
|
||||
if (!mounted) return;
|
||||
|
||||
await showFutureLoadingDialog(context: context, future: room.leave);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => leaveRoom(room),
|
||||
);
|
||||
|
||||
return;
|
||||
case ChatContextAction.addToSpace:
|
||||
@@ -901,6 +909,65 @@ class ChatListController extends State<ChatList>
|
||||
|
||||
bool waitForFirstSync = false;
|
||||
|
||||
/// Leaves the room locally and removes it from the local database.
|
||||
Future<void> leaveRoom(Room room) async {
|
||||
try {
|
||||
await room.leave();
|
||||
} on MatrixException catch (e, s) {
|
||||
// If the room was already left on the server (e.g. a stale local copy
|
||||
// reappeared after a restart) the leave request fails, but the user
|
||||
// intent (room not being listed anymore) is already fulfilled. Clean up
|
||||
// locally instead of showing an error.
|
||||
if (!{
|
||||
MatrixError.M_FORBIDDEN,
|
||||
MatrixError.M_NOT_FOUND,
|
||||
MatrixError.M_UNKNOWN,
|
||||
}.contains(e.error)) {
|
||||
Logs().w('Failed to leave room ${room.id}', e, s);
|
||||
rethrow;
|
||||
}
|
||||
Logs().w('Room ${room.id} is already gone, cleaning up locally...', e, s);
|
||||
}
|
||||
final client = room.client;
|
||||
// Make sure the room does not reappear from the local database after an
|
||||
// app restart, even if the leave sync did not run in time.
|
||||
await client.database.forgetRoom(room.id);
|
||||
client.rooms.remove(room);
|
||||
}
|
||||
|
||||
/// Loads the shared [roomTypeStateEvent] marker from the synced room state
|
||||
/// (from the local database for partially loaded rooms) and writes it for
|
||||
/// existing canal/mural rooms that only have the per-account tag of the
|
||||
/// creating account. This makes the category visible right after the sync,
|
||||
/// without having to open the room first. Only admins can write the marker,
|
||||
/// non-admin members simply read it.
|
||||
Future<void> backfillRoomTypeMarkers() async {
|
||||
final client = Matrix.of(context).client;
|
||||
if (client.rooms.isEmpty) return;
|
||||
for (final room in client.rooms) {
|
||||
if (room.membership != Membership.join) continue;
|
||||
if (room.roomType != null) continue;
|
||||
// Partial rooms do not keep unimportant state events (like our room type
|
||||
// marker) in memory, so load them from the local database first.
|
||||
await room.postLoad();
|
||||
final type =
|
||||
room.roomType ??
|
||||
(room.tags.keys.contains(AppConfig.canalTag)
|
||||
? 'canal'
|
||||
: room.tags.keys.contains(AppConfig.muralTag)
|
||||
? 'mural'
|
||||
: null);
|
||||
if (type == null || room.roomType == type) continue;
|
||||
if (room.ownPowerLevel < PowerLevel.admin) continue;
|
||||
try {
|
||||
await room.setRoomType(type);
|
||||
} catch (e, s) {
|
||||
Logs().w('Failed to backfill room type $type for ${room.id}', e, s);
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _waitForFirstSync() async {
|
||||
final router = GoRouter.of(context);
|
||||
final client = Matrix.of(context).client;
|
||||
@@ -921,6 +988,7 @@ class ChatListController extends State<ChatList>
|
||||
setState(() {
|
||||
waitForFirstSync = true;
|
||||
});
|
||||
backfillRoomTypeMarkers();
|
||||
|
||||
if (client.userDeviceKeys[client.userID!]?.deviceKeys.values.any(
|
||||
(device) => !device.verified && !device.blocked,
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:cupertino_ui/cupertino_ui.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/dummy_chat_list_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/search_title.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/room_extension.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/adaptive_dialogs/public_room_dialog.dart';
|
||||
import 'package:tatchat/widgets/avatar.dart';
|
||||
@@ -59,8 +59,9 @@ class ChatListViewBody extends StatelessWidget {
|
||||
.where(
|
||||
(room) =>
|
||||
!room.isSpace &&
|
||||
!room.tags.keys.contains(AppConfig.canalTag) &&
|
||||
!room.tags.keys.contains(AppConfig.muralTag) &&
|
||||
!room.isLeftOrBanned &&
|
||||
!room.isCanal &&
|
||||
!room.isMural &&
|
||||
spaceDelegateCandidates[room.id] == null,
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -29,6 +29,8 @@ class ChatListItem extends StatelessWidget {
|
||||
final void Function()? onForget;
|
||||
final void Function() onTap;
|
||||
final String? filter;
|
||||
final Uri? displayAvatar;
|
||||
final String? displayAvatarName;
|
||||
|
||||
const ChatListItem(
|
||||
this.room, {
|
||||
@@ -38,6 +40,8 @@ class ChatListItem extends StatelessWidget {
|
||||
this.onForget,
|
||||
this.filter,
|
||||
this.space,
|
||||
this.displayAvatar,
|
||||
this.displayAvatarName,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -148,11 +152,11 @@ class ChatListItem extends StatelessWidget {
|
||||
AppConfig.spaceBorderRadius,
|
||||
)
|
||||
: null,
|
||||
mxContent: room.avatar,
|
||||
mxContent: displayAvatar ?? room.avatar,
|
||||
size: space != null
|
||||
? Avatar.defaultSize * 0.75
|
||||
: Avatar.defaultSize,
|
||||
name: displayname,
|
||||
name: displayAvatarName ?? displayname,
|
||||
presenceUserId: directChatMatrixId,
|
||||
presenceBackgroundColor: backgroundColor,
|
||||
onTap: () => onLongPress?.call(context),
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_item.dart';
|
||||
@@ -13,6 +12,7 @@ import 'package:tatchat/pages/chat_list/tabs/chat_tab_header.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/empty_tab_view.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/room_extension.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/matrix.dart';
|
||||
|
||||
@@ -63,7 +63,8 @@ class _ChannelsTabState extends State<ChannelsTab> {
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final channels = client.rooms
|
||||
.where((room) => room.tags.keys.contains(AppConfig.canalTag))
|
||||
.where((room) => !room.isLeftOrBanned)
|
||||
.where((room) => room.isCanal)
|
||||
.where(
|
||||
(room) =>
|
||||
searchText.isEmpty ||
|
||||
@@ -105,4 +106,4 @@ class _ChannelsTabState extends State<ChannelsTab> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_item.dart';
|
||||
@@ -13,6 +12,7 @@ import 'package:tatchat/pages/chat_list/tabs/chat_tab_header.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/empty_tab_view.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/room_extension.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/matrix.dart';
|
||||
|
||||
@@ -63,7 +63,8 @@ class _MuralsTabState extends State<MuralsTab> {
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final murals = client.rooms
|
||||
.where((room) => room.tags.keys.contains(AppConfig.muralTag))
|
||||
.where((room) => !room.isLeftOrBanned)
|
||||
.where((room) => room.isMural)
|
||||
.where(
|
||||
(room) =>
|
||||
searchText.isEmpty ||
|
||||
@@ -90,11 +91,24 @@ class _MuralsTabState extends State<MuralsTab> {
|
||||
itemCount: murals.length,
|
||||
itemBuilder: (context, i) {
|
||||
final room = murals[i];
|
||||
final creatorId = room.creatorUserIds.isEmpty
|
||||
? null
|
||||
: room.creatorUserIds.first;
|
||||
final creatorState = creatorId == null
|
||||
? null
|
||||
: room.getState(EventTypes.RoomMember, creatorId);
|
||||
return ChatListItem(
|
||||
room,
|
||||
key: Key('mural_list_item_${room.id}'),
|
||||
onTap: () => widget.controller.onChatTap(room),
|
||||
activeChat: widget.controller.activeChat == room.id,
|
||||
displayAvatar: Uri.tryParse(
|
||||
creatorState?.content.tryGet<String>('avatar_url') ??
|
||||
'',
|
||||
),
|
||||
displayAvatarName: creatorState?.content.tryGet<String>(
|
||||
'displayname',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user