- 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.
258 lines
11 KiB
Dart
258 lines
11 KiB
Dart
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
|
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:cupertino_ui/cupertino_ui.dart';
|
|
import 'package:tatchat/config/setting_keys.dart';
|
|
import 'package:tatchat/config/themes.dart';
|
|
import 'package:tatchat/l10n/l10n.dart';
|
|
import 'package:tatchat/pages/chat/chat.dart';
|
|
import 'package:tatchat/pages/chat/encryption_info.dart';
|
|
import 'package:tatchat/pages/chat/events/message.dart';
|
|
import 'package:tatchat/pages/chat/seen_by_row.dart';
|
|
import 'package:tatchat/pages/chat/typing_indicators.dart';
|
|
import 'package:tatchat/utils/account_config.dart';
|
|
import 'package:tatchat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
|
import 'package:tatchat/utils/platform_infos.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:matrix/matrix_api_lite/model/event_types.dart';
|
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
|
|
|
import '../../config/app_config.dart';
|
|
import '../../utils/date_time_extension.dart';
|
|
import '../../utils/matrix_sdk_extensions/room_extension.dart';
|
|
|
|
class ChatEventList extends StatelessWidget {
|
|
final ChatController controller;
|
|
|
|
const ChatEventList({super.key, required this.controller});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final timeline = controller.timeline;
|
|
|
|
if (timeline == null) {
|
|
return const Center(child: CupertinoActivityIndicator());
|
|
}
|
|
final theme = Theme.of(context);
|
|
|
|
final colors = [theme.secondaryBubbleColor, theme.bubbleColor];
|
|
|
|
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
|
|
|
final events = timeline.events.filterByVisibleInGui(
|
|
threadId: controller.activeThreadId,
|
|
);
|
|
|
|
// create a map of eventId --> index to greatly improve performance of
|
|
// ListView's findChildIndexCallback
|
|
final thisEventsKeyMap = <String, int>{};
|
|
for (var i = 0; i < events.length; i++) {
|
|
thisEventsKeyMap[events[i].eventId] = i;
|
|
}
|
|
|
|
final hasWallpaper =
|
|
controller.room.client.applicationAccountConfig.wallpaperUrl != null;
|
|
|
|
return SelectionArea(
|
|
child: MediaQuery(
|
|
data: MediaQuery.of(context).copyWith(
|
|
textScaler: TextScaler.linear(AppSettings.fontSizeFactor.value),
|
|
),
|
|
child: ListView.custom(
|
|
padding: EdgeInsets.only(
|
|
top: 8 + MediaQuery.paddingOf(context).top,
|
|
bottom:
|
|
8 +
|
|
(controller.inputBarHeight ??
|
|
(72 + (FluffyThemes.isColumnMode(context) ? 16 : 0))),
|
|
left: horizontalPadding,
|
|
right: horizontalPadding,
|
|
),
|
|
reverse: true,
|
|
controller: controller.scrollController,
|
|
keyboardDismissBehavior: PlatformInfos.isIOS
|
|
? ScrollViewKeyboardDismissBehavior.onDrag
|
|
: ScrollViewKeyboardDismissBehavior.manual,
|
|
childrenDelegate: SliverChildBuilderDelegate(
|
|
(BuildContext context, int i) {
|
|
// Footer to display typing indicator and read receipts:
|
|
if (i == 0) {
|
|
if (timeline.canRequestFuture) {
|
|
return Center(
|
|
child: TextButton.icon(
|
|
onPressed: timeline.isRequestingFuture
|
|
? null
|
|
: controller.requestFuture,
|
|
icon: timeline.isRequestingFuture
|
|
? CircularProgressIndicator.adaptive(strokeWidth: 2)
|
|
: const Icon(Icons.arrow_downward_outlined),
|
|
label: Text(L10n.of(context).loadMore),
|
|
),
|
|
);
|
|
}
|
|
return Column(
|
|
mainAxisSize: .min,
|
|
children: [
|
|
if (events.isNotEmpty) SeenByRow(event: events.first),
|
|
TypingIndicators(controller),
|
|
EncryptionInfo(room: controller.room),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Request history button or progress indicator:
|
|
if (i == events.length + 1) {
|
|
if (controller.activeThreadId != null ||
|
|
!timeline.canRequestHistory) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return Builder(
|
|
builder: (context) {
|
|
final visibleIndex = timeline.events.lastIndexWhere(
|
|
(event) =>
|
|
!event.isCollapsedState && event.isVisibleInGui,
|
|
);
|
|
if (visibleIndex > timeline.events.length - 50) {
|
|
WidgetsBinding.instance.addPostFrameCallback(
|
|
controller.requestHistory,
|
|
);
|
|
}
|
|
return Center(
|
|
child: TextButton.icon(
|
|
onPressed: timeline.isRequestingHistory
|
|
? null
|
|
: controller.requestHistory,
|
|
icon: timeline.isRequestingHistory
|
|
? CircularProgressIndicator.adaptive(strokeWidth: 2)
|
|
: const Icon(Icons.arrow_upward_outlined),
|
|
label: Text(L10n.of(context).loadMore),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
i--;
|
|
|
|
// The message at this index:
|
|
final event = events[i];
|
|
final animateIn =
|
|
(event.transactionId ?? event.eventId) ==
|
|
controller.animateInEventId;
|
|
|
|
final nextEvent = i + 1 < events.length ? events[i + 1] : null;
|
|
final previousEvent = i > 0 ? events[i - 1] : null;
|
|
|
|
// Collapsed state event
|
|
final canExpand =
|
|
event.isCollapsedState &&
|
|
nextEvent?.isCollapsedState == true &&
|
|
previousEvent?.isCollapsedState != true;
|
|
final isCollapsed =
|
|
event.isCollapsedState &&
|
|
previousEvent?.isCollapsedState == true &&
|
|
!controller.expandedEventIds.contains(event.eventId);
|
|
|
|
final displayDate =
|
|
event.type == EventTypes.RoomCreate ||
|
|
nextEvent == null ||
|
|
!event.originServerTs.sameDay(nextEvent.originServerTs);
|
|
|
|
return AutoScrollTag(
|
|
key: ValueKey(event.transactionId ?? event.eventId),
|
|
index: i,
|
|
controller: controller.scrollController,
|
|
child: Column(
|
|
mainAxisSize: .min,
|
|
children: [
|
|
if (!isCollapsed && displayDate)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0, bottom: 16.0),
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 4.0),
|
|
child: Material(
|
|
borderRadius: BorderRadius.circular(
|
|
AppConfig.borderRadius * 2,
|
|
),
|
|
color: theme.colorScheme.inverseSurface.withAlpha(
|
|
200,
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8.0,
|
|
vertical: 2.0,
|
|
),
|
|
child: Text(
|
|
event.originServerTs.localizedDate(context),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.onInverseSurface,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Message(
|
|
event,
|
|
bigEmojis: controller.bigEmojis,
|
|
animateIn: animateIn,
|
|
onSwipe: () => controller.replyAction(replyTo: event),
|
|
onInfoTab: controller.showEventInfo,
|
|
onMention: () => controller.sendController.text +=
|
|
'${event.senderFromMemoryOrFallback.mention} ',
|
|
highlightMarker:
|
|
controller.scrollToEventIdMarker == event.eventId,
|
|
onSelect: controller.onSelectMessage,
|
|
scrollToEventId: controller.scrollToEventId,
|
|
longPressSelect: controller.selectedEvents.isNotEmpty,
|
|
selected: controller.selectedEvents.any(
|
|
(e) => e.eventId == event.eventId,
|
|
),
|
|
singleSelected:
|
|
controller.selectedEvents.singleOrNull?.eventId ==
|
|
event.eventId,
|
|
onEdit: controller.editSelectedEventAction,
|
|
timeline: timeline,
|
|
displayReadMarker:
|
|
i > 0 &&
|
|
controller.readMarkerEventId == event.eventId,
|
|
nextEvent: nextEvent,
|
|
previousEvent: previousEvent,
|
|
wallpaperMode: hasWallpaper,
|
|
scrollController: controller.scrollController,
|
|
colors: colors,
|
|
isCollapsed: isCollapsed,
|
|
enterThread: controller.activeThreadId == null
|
|
? controller.enterThread
|
|
: null,
|
|
onExpand: canExpand
|
|
? () => controller.expandEventsFrom(
|
|
event,
|
|
!controller.expandedEventIds.contains(
|
|
event.eventId,
|
|
),
|
|
)
|
|
: null,
|
|
fullWidthCard:
|
|
controller.activeThreadId == null &&
|
|
controller.room.isCanal,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
childCount: events.length + 2,
|
|
findChildIndexCallback: (key) =>
|
|
controller.findChildIndexCallback(key, thisEventsKeyMap),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|