feat: cartões de publicação em canais e corrreções de salas arquivadas
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled

- 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:
2026-09-14 08:30:25 -03:00
parent 41a6a52393
commit ebafa1dc39
29 changed files with 10543 additions and 9965 deletions

View File

@@ -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,