Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions lib/app/layouts/conversation_view/pages/conversation_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ class ConversationViewState extends OptimizedState<ConversationView> {
canPop: false,
onPopInvoked: (didPop) async {
if (didPop) return;
if (controller.keyboardOpen ||
controller.focusNode.hasFocus ||
controller.subjectFocusNode.hasFocus) {
controller.dismissKeyboard();
return;
}
if (controller.inSelectMode.value) {
controller.inSelectMode.value = false;
controller.selected.clear();
Expand Down Expand Up @@ -155,10 +161,20 @@ class ConversationViewState extends OptimizedState<ConversationView> {
Expanded(
child: Stack(
children: [
MessagesView(
key: Key(chat.guid),
customService: widget.customService,
controller: controller,
Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) {
if (controller.keyboardOpen ||
controller.focusNode.hasFocus ||
controller.subjectFocusNode.hasFocus) {
controller.dismissKeyboard();
}
},
child: MessagesView(
key: Key(chat.guid),
customService: widget.customService,
controller: controller,
),
),
Align(
alignment: iOS ? Alignment.bottomRight : Alignment.bottomCenter,
Expand Down Expand Up @@ -218,8 +234,7 @@ class ConversationViewState extends OptimizedState<ConversationView> {
if (ss.settings.swipeToCloseKeyboard.value &&
details.delta.dy > 0 &&
controller.keyboardOpen) {
controller.focusNode.unfocus();
controller.subjectFocusNode.unfocus();
controller.dismissKeyboard();
} else if (ss.settings.swipeToOpenKeyboard.value &&
details.delta.dy < 0 &&
!controller.keyboardOpen) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/popup/
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/reaction/reaction_holder.dart';
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/reply/reply_bubble.dart';
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/reply/reply_line_painter.dart';
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/reply/reply_thread_popup.dart';
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/text/text_bubble.dart';
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/timestamp/delivered_indicator.dart';
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/timestamp/message_timestamp.dart';
Expand Down Expand Up @@ -182,6 +183,12 @@ class _MessageHolderState extends CustomState<MessageHolder, void, MessageWidget
Iterable<Message> reactionsForPart(int part) {
return reactions.where((s) => (s.associatedMessagePart ?? 0) == part);
}
final replyTarget = replyTo;
MessageWidgetController? replyController;
if (replyTarget?.guid != null) {
replyController = getActiveMwc(replyTarget!.guid!) ?? mwc(replyTarget);
replyController.cvController ??= widget.cvController;
}
/// Layout tree
/// - Timestamp
/// - Stack (see code comment)
Expand Down Expand Up @@ -272,12 +279,12 @@ class _MessageHolderState extends CustomState<MessageHolder, void, MessageWidget
&& olderMessage != null
&& message.threadOriginatorGuid != null
&& message.showUpperMessage(olderMessage!)
&& replyTo != null
&& getActiveMwc(replyTo!.guid!) != null)
&& replyTarget != null
&& replyController != null)
Padding(
padding: EdgeInsets.only(left: (showAvatar || ss.settings.alwaysShowAvatars.value) && replyTo!.isFromMe! ? 35 : 0),
padding: EdgeInsets.only(left: (showAvatar || ss.settings.alwaysShowAvatars.value) && replyTarget.isFromMe! ? 35 : 0),
child: DecoratedBox(
decoration: replyTo!.isFromMe == message.isFromMe ? ReplyLineDecoration(
decoration: replyTarget.isFromMe == message.isFromMe ? ReplyLineDecoration(
isFromMe: message.isFromMe!,
color: context.theme.colorScheme.properSurface,
connectUpper: false,
Expand All @@ -286,11 +293,11 @@ class _MessageHolderState extends CustomState<MessageHolder, void, MessageWidget
) : const BoxDecoration(),
child: Container(
width: double.infinity,
alignment: replyTo!.isFromMe! ? Alignment.centerRight : Alignment.centerLeft,
alignment: replyTarget.isFromMe! ? Alignment.centerRight : Alignment.centerLeft,
child: ReplyBubble(
parentController: getActiveMwc(replyTo!.guid!)!,
part: replyTo!.guid! == message.threadOriginatorGuid ? message.normalizedThreadPart : 0,
showAvatar: (chat.isGroup || ss.settings.alwaysShowAvatars.value || !iOS) && !replyTo!.isFromMe!,
parentController: replyController,
part: replyTarget.guid! == message.threadOriginatorGuid ? message.normalizedThreadPart : 0,
showAvatar: (chat.isGroup || ss.settings.alwaysShowAvatars.value || !iOS) && !replyTarget.isFromMe!,
cvController: widget.cvController,
),
),
Expand All @@ -312,8 +319,8 @@ class _MessageHolderState extends CustomState<MessageHolder, void, MessageWidget
if (!iOS && index == 0 && !widget.isReplyThread
&& olderMessage != null
&& message.threadOriginatorGuid != null
&& replyTo != null
&& getActiveMwc(replyTo!.guid!) != null)
&& replyTarget != null
&& replyController != null)
Padding(
padding: showAvatar || ss.settings.alwaysShowAvatars.value
? const EdgeInsets.only(left: 45.0, right: 10) : const EdgeInsets.symmetric(horizontal: 10),
Expand All @@ -323,10 +330,10 @@ class _MessageHolderState extends CustomState<MessageHolder, void, MessageWidget
border: Border.fromBorderSide(BorderSide(color: context.theme.colorScheme.properSurface)),
),
child: ReplyBubble(
parentController: getActiveMwc(replyTo!.guid!)!,
part: replyTo!.guid! == message.threadOriginatorGuid ? message.normalizedThreadPart : 0,
parentController: replyController,
part: replyTarget.guid! == message.threadOriginatorGuid ? message.normalizedThreadPart : 0,
showAvatar: (chat.isGroup || ss.settings.alwaysShowAvatars.value || !iOS)
&& !replyTo!.isFromMe!,
&& !replyTarget.isFromMe!,
cvController: widget.cvController,
),
),
Expand Down Expand Up @@ -369,6 +376,8 @@ class _MessageHolderState extends CustomState<MessageHolder, void, MessageWidget
} else {
widget.cvController.selected.add(message);
}
} : message.threadOriginatorGuid != null && !widget.isReplyThread ? () {
showReplyThread(context, message, e, service, widget.cvController);
} : kIsDesktop || kIsWeb || iOS || material ? () => tapped.value = !tapped.value : null,
child: IgnorePointer(
ignoring: widget.cvController.inSelectMode.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import 'package:defer_pointer/defer_pointer.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:universal_io/io.dart';

Expand All @@ -34,8 +33,7 @@ class ReactionWidget extends StatefulWidget {

class ReactionWidgetState extends OptimizedState<ReactionWidget> {
late Message reaction = widget.reaction;
late final StreamSubscription sub;
bool hasStream = false;
StreamSubscription? sub;

List<Message>? get reactions => widget.reactions;
bool get reactionIsFromMe => reaction.isFromMe!;
Expand Down Expand Up @@ -70,8 +68,6 @@ class ReactionWidgetState extends OptimizedState<ReactionWidget> {
getActiveMwc(widget.message!.guid!)?.updateAssociatedMessage(reaction, updateHolder: false);
}
});

hasStream = true;
} else if (kIsWeb && widget.message != null) {
sub = WebListeners.messageUpdate.listen((tuple) {
final _message = tuple.item1;
Expand Down Expand Up @@ -125,7 +121,7 @@ class ReactionWidgetState extends OptimizedState<ReactionWidget> {

@override
void dispose() {
if (!kIsWeb && hasStream) sub.cancel();
sub?.cancel();
super.dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,43 @@ import 'package:bluebubbles/helpers/helpers.dart';
import 'package:bluebubbles/database/database.dart';
import 'package:bluebubbles/database/models.dart';
import 'package:bluebubbles/services/services.dart';
import 'package:collection/collection.dart';
import 'package:defer_pointer/defer_pointer.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';

void showReplyThread(BuildContext context, Message message, MessagePart part, MessagesService service, ConversationViewController cvController) {
Future<void> showReplyThread(BuildContext context, Message message, MessagePart part, MessagesService service, ConversationViewController cvController) async {
cvController.dismissKeyboard();
final originatorPart = message.threadOriginatorGuid != null ? message.normalizedThreadPart : part.part;
final _messages = service.struct.threads(message.threadOriginatorGuid ?? message.guid!, originatorPart);
final originatorGuid = message.threadOriginatorGuid ?? message.guid!;
final messagesByGuid = <String, Message>{};

if (!kIsWeb) {
final originator = Message.findOne(guid: originatorGuid);
final query = Database.messages.query(Message_.threadOriginatorGuid.equals(originatorGuid)).build();
final storedReplies = query.find();
query.close();

for (final stored in [
if (originator != null) originator,
...storedReplies,
]) {
if (stored.guid == null || stored.associatedMessageGuid != null) continue;
if (stored.guid != originatorGuid && stored.normalizedThreadPart != originatorPart) continue;
stored.fetchAttachments();
stored.fetchAssociatedMessages(service: service);
stored.handle = stored.getHandle();
messagesByGuid[stored.guid!] = stored;
}
}

// Prefer live in-memory messages when both sources contain the same item.
for (final live in service.struct.threads(originatorGuid, originatorPart)) {
if (live.guid != null) messagesByGuid[live.guid!] = live;
}
final _messages = messagesByGuid.values.toList();
_messages.sort((a, b) => Message.sort(a, b, descending: false));
_buildThreadView(_messages, originatorPart, cvController, context);
}
Expand Down Expand Up @@ -92,39 +118,40 @@ void _buildThreadView(List<Message> _messages, int? originatorPart, Conversation
),
Container(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: SingleChildScrollView(
controller: controller,
child: Column(
children: _messages.mapIndexed((index, e) => GestureDetector(
onTap: () {
Navigator.of(context).pop();
if (originatorPart == null && ss.settings.skin.value == Skins.iOS) {
// pop twice to remove convo details page
Navigator.of(context).pop();
}
ms(cvController.chat.guid).jumpToMessage.call(e.guid!);
},
child: AbsorbPointer(
absorbing: true,
child: Padding(
padding: const EdgeInsets.only(left: 5.0, right: 5.0),
child: MessageHolder(
cvController: cvController,
message: _messages[index],
oldMessageGuid: index > 0 ? _messages[index - 1].guid : null,
newMessageGuid: index < _messages.length - 1 ? _messages[index + 1].guid : null,
isReplyThread: true,
replyPart: index == 0 ? originatorPart : null,
),
),
child: ListView.builder(
controller: controller,
padding: const EdgeInsets.symmetric(vertical: 16),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
itemCount: _messages.length,
itemBuilder: (context, index) {
final threadMessage = _messages[index];
final messageController = getActiveMwc(threadMessage.guid!) ?? mwc(threadMessage);
messageController.cvController = cvController;
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
if (originatorPart == null && ss.settings.skin.value == Skins.iOS) {
// pop twice to remove convo details page
Navigator.of(context).pop();
}
ms(cvController.chat.guid).jumpToMessage.call(threadMessage.guid!);
},
child: AbsorbPointer(
absorbing: true,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: MessageHolder(
cvController: cvController,
message: threadMessage,
oldMessageGuid: index > 0 ? _messages[index - 1].guid : null,
newMessageGuid: index < _messages.length - 1 ? _messages[index + 1].guid : null,
isReplyThread: true,
replyPart: index == 0 ? originatorPart : null,
),
)).toList(),
),
),
),
),
);
},
),
),
),
Expand Down
24 changes: 19 additions & 5 deletions lib/database/global/chat_messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ class ChatMessages {
List<Message> get messages => _messages.values.toList();
List<Message> get reactions => _reactions.values.toList();
List<Attachment> get attachments => _attachments.values.toList();
List<Message> threads(String originatorGuid, int originatorPart, {bool returnOriginator = true}) =>
_threads[originatorGuid]?.values.where((e) =>
(e.normalizedThreadPart == originatorPart && e.guid != originatorGuid) || (returnOriginator ? e.guid == originatorGuid : false)).toList() ?? [];
List<Message> threads(String originatorGuid, int originatorPart, {bool returnOriginator = true}) {
final thread = _threads[originatorGuid];
if (returnOriginator && thread?[originatorGuid] == null) {
final originator = _messages[originatorGuid];
if (originator != null) {
addThreadOriginator(originator);
}
}
return _threads[originatorGuid]?.values.where((e) =>
(e.normalizedThreadPart == originatorPart && e.guid != originatorGuid) ||
(returnOriginator && e.guid == originatorGuid)).toList() ?? [];
}

void addMessages(List<Message> __messages) {
for (Message m in __messages) {
Expand All @@ -27,8 +36,13 @@ class ChatMessages {
}
if (m.threadOriginatorGuid != null && !m.guid!.startsWith("temp") && m.associatedMessageGuid == null) {
// add threaded messages
_threads[m.threadOriginatorGuid!] ??= {};
_threads[m.threadOriginatorGuid]![m.guid!] = m;
final originatorGuid = m.threadOriginatorGuid!;
_threads[originatorGuid] ??= {};
_threads[originatorGuid]![m.guid!] = m;
final loadedOriginator = _messages[originatorGuid];
if (loadedOriginator != null) {
_threads[originatorGuid]![originatorGuid] = loadedOriginator;
}
}
if (_threads.keys.contains(m.guid)) {
// add thread 'originator'
Expand Down
Loading