Initial commit
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2026-09-11 15:59:56 -03:00
commit 0037b0b9cf
651 changed files with 199705 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
// 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';
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,
),
],
),
);
},
childCount: events.length + 2,
findChildIndexCallback: (key) =>
controller.findChildIndexCallback(key, thisEventsKeyMap),
),
),
),
);
}
}