fluffychat merge

This commit is contained in:
ggurdin 2026-02-04 15:56:35 -05:00
commit 6d8f347a81
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
8 changed files with 246 additions and 95 deletions

View file

@ -3431,7 +3431,7 @@
"addAnswerOption": "Add answer option",
"allowMultipleAnswers": "Allow multiple answers",
"pollHasBeenEnded": "Poll has been ended",
"countVotes": "{count} votes",
"countVotes": "{count, plural, =1{One vote} other{{count} votes}}",
"@countVotes": {
"type": "int",
"placeholders": {
@ -3441,6 +3441,18 @@
}
},
"answersWillBeVisibleWhenPollHasEnded": "Answers will be visible when poll has ended",
"replyInThread": "Reply in thread",
"countReplies": "{count, plural, =1{One reply} other{{count} replies}}",
"@countReplies": {
"type": "int",
"placeholders": {
"count": {
"type": "int"
}
}
},
"thread": "Thread",
"backToMainChat": "Back to main chat",
"ignore": "Block",
"ignoredUsers": "Blocked users",
"writeAMessageLangCodes": "Type in {l1} or {l2}...",

View file

@ -202,6 +202,8 @@ class ChatController extends State<ChatPageWithRoom>
Timeline? timeline;
String? activeThreadId;
late final String readMarkerEventId;
String get roomId => widget.room.id;
@ -231,6 +233,8 @@ class ChatController extends State<ChatPageWithRoom>
// files: details.files,
// room: room,
// outerContext: context,
// threadRootEventId: activeThreadId,
// threadLastEventId: threadLastEventId,
// ),
// );
// }
@ -274,6 +278,25 @@ class ChatController extends State<ChatPageWithRoom>
bool showEmojiPicker = false;
String? get threadLastEventId {
final threadId = activeThreadId;
if (threadId == null) return null;
return timeline?.events
.filterByVisibleInGui(threadId: threadId)
.firstOrNull
?.eventId;
}
void enterThread(String eventId) => setState(() {
activeThreadId = eventId;
selectedEvents.clear();
});
void closeThread() => setState(() {
activeThreadId = null;
selectedEvents.clear();
});
void recreateChat() async {
final room = this.room;
final userId = room.directChatMatrixID;
@ -387,6 +410,8 @@ class ChatController extends State<ChatPageWithRoom>
files: files,
room: room,
outerContext: context,
threadRootEventId: activeThreadId,
threadLastEventId: threadLastEventId,
),
);
}
@ -563,7 +588,9 @@ class ChatController extends State<ChatPageWithRoom>
final Set<String> expandedEventIds = {};
void expandEventsFrom(Event event, bool expand) {
final events = timeline!.events.filterByVisibleInGui();
final events = timeline!.events.filterByVisibleInGui(
threadId: activeThreadId,
);
final start = events.indexOf(event);
setState(() {
for (var i = start; i < events.length; i++) {
@ -590,6 +617,7 @@ class ChatController extends State<ChatPageWithRoom>
: timeline!.events
.filterByVisibleInGui(
exceptionEventId: readMarkerEventId,
threadId: activeThreadId,
)
.indexWhere((e) => e.eventId == readMarkerEventId);
@ -600,6 +628,7 @@ class ChatController extends State<ChatPageWithRoom>
readMarkerEventIndex = timeline!.events
.filterByVisibleInGui(
exceptionEventId: readMarkerEventId,
threadId: activeThreadId,
)
.indexWhere((e) => e.eventId == readMarkerEventId);
}
@ -971,8 +1000,13 @@ class ChatController extends State<ChatPageWithRoom>
// room.sendTextEvent(
// sendController.text,
// inReplyTo: replyEvent,
// editEventId: editEvent?.eventId,
// editEventId: editEvent.eventId,
// parseCommands: parseCommands,
// threadRootEventId: activeThreadId,
// );
// sendController.value = TextEditingValue(
// text: pendingText,
// selection: const TextSelection.collapsed(offset: 0),
// );
// If the message and the sendController text don't match, it's possible
// that there was a delay in tokenization before send, and the user started
@ -998,6 +1032,7 @@ class ChatController extends State<ChatPageWithRoom>
tokensWritten: content.tokensWritten,
choreo: content.choreo,
txid: tempEventId,
threadRootEventId: activeThreadId,
)
.then(
(String? msgEventId) async {
@ -1092,6 +1127,8 @@ class ChatController extends State<ChatPageWithRoom>
files: files,
room: room,
outerContext: context,
threadRootEventId: activeThreadId,
threadLastEventId: threadLastEventId,
),
);
}
@ -1104,6 +1141,8 @@ class ChatController extends State<ChatPageWithRoom>
files: [XFile.fromData(image)],
room: room,
outerContext: context,
threadRootEventId: activeThreadId,
threadLastEventId: threadLastEventId,
),
);
}
@ -1120,6 +1159,8 @@ class ChatController extends State<ChatPageWithRoom>
files: [file],
room: room,
outerContext: context,
threadRootEventId: activeThreadId,
threadLastEventId: threadLastEventId,
),
);
}
@ -1139,6 +1180,8 @@ class ChatController extends State<ChatPageWithRoom>
files: [file],
room: room,
outerContext: context,
threadRootEventId: activeThreadId,
threadLastEventId: threadLastEventId,
),
);
}
@ -1192,6 +1235,7 @@ class ChatController extends State<ChatPageWithRoom>
// inReplyTo: replyEvent,
inReplyTo: replyEvent.value,
// Pangea#
threadRootEventId: activeThreadId,
extraContent: {
'info': {
...file.info,
@ -1570,6 +1614,7 @@ class ChatController extends State<ChatPageWithRoom>
: timeline!.events
.filterByVisibleInGui(
exceptionEventId: eventId,
threadId: activeThreadId,
)
.indexOf(foundEvent);

View file

@ -103,6 +103,8 @@ class ChatEmojiPicker extends StatelessWidget {
// 'url': sticker.url.toString(),
// },
// type: EventTypes.Sticker,
// threadRootEventId: controller.activeThreadId,
// threadLastEventId: controller.threadLastEventId,
// );
// controller.hideEmojiPicker();
// },

View file

@ -37,7 +37,9 @@ class ChatEventList extends StatelessWidget {
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
final events = timeline.events.filterByVisibleInGui();
final events = timeline.events.filterByVisibleInGui(
threadId: controller.activeThreadId,
);
final animateInEventIndex = controller.animateInEventIndex;
// create a map of eventId --> index to greatly improve performance of
@ -111,7 +113,8 @@ class ChatEventList extends StatelessWidget {
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
}
if (timeline.canRequestHistory) {
if (timeline.canRequestHistory &&
controller.activeThreadId == null) {
return Builder(
builder: (context) {
// #Pangea
@ -132,7 +135,6 @@ class ChatEventList extends StatelessWidget {
}
return const SizedBox.shrink();
}
// #Pangea
if (i == 1) {
return ActivityUserSummaries(controller: controller);
@ -207,6 +209,9 @@ class ChatEventList extends StatelessWidget {
scrollController: controller.scrollController,
colors: colors,
isCollapsed: isCollapsed,
enterThread: controller.activeThreadId == null
? controller.enterThread
: null,
onExpand: canExpand
? () => controller.expandEventsFrom(
event,

View file

@ -3,6 +3,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:badges/badges.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
@ -54,11 +55,15 @@ class ChatView extends StatelessWidget {
// tooltip: L10n.of(context).edit,
// onPressed: controller.editSelectedEventAction,
// ),
// IconButton(
// icon: const Icon(Icons.copy_outlined),
// tooltip: L10n.of(context).copy,
// onPressed: controller.copyEventsAction,
// ),
// if (controller.selectedEvents.length == 1 &&
// controller.activeThreadId == null &&
// controller.room.canSendDefaultMessages)
// IconButton(
// icon: const Icon(Icons.message_outlined),
// tooltip: L10n.of(context).replyInThread,
// onPressed: () => controller
// .enterThread(controller.selectedEvents.single.eventId),
// ),
// if (controller.canPinSelectedEvents)
// IconButton(
// icon: const Icon(Icons.push_pin_outlined),
@ -86,6 +91,18 @@ class ChatView extends StatelessWidget {
// }
// },
// itemBuilder: (context) => [
// PopupMenuItem(
// onTap: controller.copyEventsAction,
// value: null,
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.copy_outlined),
// const SizedBox(width: 12),
// Text(L10n.of(context).copy),
// ],
// ),
// ),
// if (controller.canSaveSelectedEvent)
// PopupMenuItem(
// onTap: () => controller.saveSelectedEvent(context),
@ -208,13 +225,17 @@ class ChatView extends StatelessWidget {
final accountConfig = Matrix.of(context).client.applicationAccountConfig;
return PopScope(
canPop: controller.selectedEvents.isEmpty && !controller.showEmojiPicker,
canPop: controller.selectedEvents.isEmpty &&
!controller.showEmojiPicker &&
controller.activeThreadId == null,
onPopInvokedWithResult: (pop, _) async {
if (pop) return;
if (controller.selectedEvents.isNotEmpty) {
controller.clearSelectedEvents();
} else if (controller.showEmojiPicker) {
controller.emojiPickerAction();
} else if (controller.activeThreadId != null) {
controller.closeThread();
}
},
child: StreamBuilder(
@ -235,6 +256,10 @@ class ChatView extends StatelessWidget {
}
// Pangea#
var appbarBottomHeight = 0.0;
final activeThreadId = controller.activeThreadId;
if (activeThreadId != null) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
if (controller.room.pinnedEventIds.isNotEmpty) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
@ -250,7 +275,9 @@ class ChatView extends StatelessWidget {
// : theme.colorScheme.onTertiaryContainer,
// ),
// backgroundColor: controller.selectedEvents.isEmpty
// ? null
// ? controller.activeThreadId != null
// ? theme.colorScheme.secondaryContainer
// : null
// : theme.colorScheme.tertiaryContainer,
// Pangea#
automaticallyImplyLeading: false,
@ -261,39 +288,49 @@ class ChatView extends StatelessWidget {
tooltip: L10n.of(context).close,
color: theme.colorScheme.onTertiaryContainer,
)
// #Pangea
: controller.widget.backButton != null
? controller.widget.backButton!
// : FluffyThemes.isColumnMode(context)
// ? null
// Pangea#
: StreamBuilder<Object>(
stream:
Matrix.of(context).client.onSync.stream.where(
: activeThreadId != null
? IconButton(
icon: const Icon(Icons.close),
onPressed: controller.closeThread,
tooltip: L10n.of(context).backToMainChat,
color: theme.colorScheme.onSecondaryContainer,
)
// #Pangea
: controller.widget.backButton != null
? controller.widget.backButton!
// : FluffyThemes.isColumnMode(context)
// ? null
// Pangea#
: StreamBuilder<Object>(
stream: Matrix.of(context)
.client
.onSync
.stream
.where(
(syncUpdate) => syncUpdate.hasRoomUpdate,
),
// #Pangea
// builder: (context, _) => UnreadRoomsBadge(
// filter: (r) => r.id != controller.roomId,
// badgePosition:
// BadgePosition.topEnd(end: 8, top: 4),
// child: const Center(child: BackButton()),
// ),
builder: (context, _) => Center(
child: SizedBox(
height: kToolbarHeight,
child: UnreadRoomsBadge(
filter: (r) => r.id != controller.roomId,
badgePosition: BadgePosition.topEnd(
end: 8,
top: 9,
// #Pangea
// builder: (context, _) => UnreadRoomsBadge(
// filter: (r) => r.id != controller.roomId,
// badgePosition:
// BadgePosition.topEnd(end: 8, top: 4),
// child: const Center(child: BackButton()),
// ),
builder: (context, _) => Center(
child: SizedBox(
height: kToolbarHeight,
child: UnreadRoomsBadge(
filter: (r) => r.id != controller.roomId,
badgePosition: BadgePosition.topEnd(
end: 8,
top: 9,
),
child: const Center(child: BackButton()),
),
),
child: const Center(child: BackButton()),
),
// Pangea#
),
),
// Pangea#
),
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
title: ChatAppBarTitle(controller),
actions: _appBarActions(context),
@ -302,6 +339,18 @@ class ChatView extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (activeThreadId != null)
SizedBox(
height: ChatAppBarListTile.fixedHeight,
child: Center(
child: TextButton.icon(
onPressed: () =>
controller.scrollToEventId(activeThreadId),
icon: const Icon(Icons.message),
label: Text(L10n.of(context).replyInThread),
),
),
),
PinnedEvents(controller),
if (scrollUpBannerEventId != null)
ChatAppBarListTile(
@ -548,7 +597,7 @@ class ChatView extends StatelessWidget {
// ],
// ),
// ),
// )
// ),
else if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join &&
(controller.room.activityPlan == null ||

View file

@ -22,6 +22,7 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -42,6 +43,7 @@ class Message extends StatelessWidget {
final void Function() onSwipe;
final void Function() onMention;
final void Function() onEdit;
final void Function(String eventId)? enterThread;
final bool longPressSelect;
final bool selected;
final bool singleSelected;
@ -82,6 +84,7 @@ class Message extends StatelessWidget {
required this.scrollController,
required this.colors,
this.onExpand,
required this.enterThread,
this.isCollapsed = false,
// #Pangea
required this.controller,
@ -296,11 +299,16 @@ class Message extends StatelessWidget {
final showReceiptsRow =
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
final threadChildren =
event.aggregatedEvents(timeline, RelationshipTypes.thread);
// #Pangea
// final showReactionPicker =
// singleSelected && event.room.canSendDefaultMessages;
// Pangea#
final enterThread = this.enterThread;
return Center(
child: Swipeable(
key: ValueKey(event.eventId),
@ -605,6 +613,29 @@ class Message extends StatelessWidget {
child: ValueListenableBuilder(
valueListenable: controller
.depressMessageButton,
builder: (
context,
depressed,
child,
) =>
PressableButton(
buttonHeight: 5,
depressed: !isButton ||
depressed,
borderRadius:
borderRadius,
onPressed: () {
showToolbar(
pangeaMessageEvent,
);
},
color: color,
visible:
isButton && !noBubble,
builder:
(context, _, __) =>
child!,
),
// Pangea#
child: Container(
@ -641,14 +672,6 @@ class Message extends StatelessWidget {
scrollController:
scrollController,
child: Container(
// #Pangea
key: MatrixState
.pAnyState
.layerLinkAndKey(
event.eventId,
)
.key,
// Pangea#
decoration:
BoxDecoration(
borderRadius:
@ -672,15 +695,10 @@ class Message extends StatelessWidget {
CrossAxisAlignment
.start,
children: <Widget>[
if ({
RelationshipTypes
.reply,
RelationshipTypes
.thread,
}.contains(
event
.relationshipType,
))
if (RelationshipTypes
.reply ==
event
.relationshipType)
FutureBuilder<
Event?>(
future: event
@ -702,11 +720,7 @@ class Message extends StatelessWidget {
'msgtype': 'm.text',
'body': '...',
},
// #Pangea
// senderId: event
// .senderId,
senderId: "",
// Pangea#
senderId: event.senderId,
type: 'm.room.message',
room: event.room,
status: EventStatus.sent,
@ -833,31 +847,6 @@ class Message extends StatelessWidget {
),
),
),
// #Pangea
builder: (
context,
depressed,
child,
) =>
PressableButton(
buttonHeight: 5,
depressed: !isButton ||
depressed,
borderRadius:
borderRadius,
onPressed: () {
showToolbar(
pangeaMessageEvent,
);
},
color: color,
visible:
isButton && !noBubble,
builder:
(context, _, __) =>
child!,
),
// Pangea#
),
),
),
@ -1094,6 +1083,38 @@ class Message extends StatelessWidget {
// child: MessageReactions(event, timeline),
// ),
// ),
if (enterThread != null)
AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: Alignment.bottomCenter,
child: threadChildren.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.only(top: 1.0, bottom: 4.0),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
),
child: TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
theme.colorScheme.surfaceContainerHighest,
),
onPressed: () => enterThread(event.eventId),
icon: const Icon(Icons.message),
label: Text(
'${L10n.of(context).countReplies(threadChildren.length)} | ${threadChildren.first.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
withSenderNamePrefix: true,
)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
),
!showReceiptsRow
? const SizedBox.shrink()
: Padding(

View file

@ -20,11 +20,14 @@ class SendFileDialog extends StatefulWidget {
final Room room;
final List<XFile> files;
final BuildContext outerContext;
final String? threadLastEventId, threadRootEventId;
const SendFileDialog({
required this.room,
required this.files,
required this.outerContext,
required this.threadLastEventId,
required this.threadRootEventId,
super.key,
});
@ -108,6 +111,8 @@ class SendFileDialogState extends State<SendFileDialog> {
thumbnail: thumbnail,
shrinkImageMaxDimension: compress ? 1600 : null,
extraContent: label.isEmpty ? null : {'body': label},
threadRootEventId: widget.threadRootEventId,
threadLastEventId: widget.threadLastEventId,
);
} on MatrixException catch (e) {
final retryAfterMs = e.retryAfterMs;

View file

@ -9,14 +9,26 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extensi
extension VisibleInGuiExtension on List<Event> {
List<Event> filterByVisibleInGui({
String? exceptionEventId,
String? threadId,
}) =>
where(
// #Pangea
// (event) => event.isVisibleInGui || event.eventId == exceptionEventId,
(event) =>
(event.isVisibleInGui || event.eventId == exceptionEventId) &&
event.isVisibleInPangeaGui,
// Pangea#
(event) {
if (threadId != null &&
event.relationshipType != RelationshipTypes.reaction) {
if ((event.relationshipType != RelationshipTypes.thread ||
event.relationshipEventId != threadId) &&
event.eventId != threadId) {
return false;
}
} else if (event.relationshipType == RelationshipTypes.thread) {
return false;
}
// #Pangea
// return event.isVisibleInGui || event.eventId == exceptionEventId;
return (event.isVisibleInGui || event.eventId == exceptionEventId) &&
event.isVisibleInPangeaGui;
// Pangea#
},
).toList();
}