moxxy/lib/ui/bloc/conversation_bloc.dart

357 lines
11 KiB
Dart

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/events.dart' as events;
import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
import 'package:moxxyv2/ui/bloc/sendfiles_bloc.dart';
import 'package:moxxyv2/ui/bloc/sharedmedia_bloc.dart';
import 'package:moxxyv2/ui/constants.dart';
part 'conversation_bloc.freezed.dart';
part 'conversation_event.dart';
part 'conversation_state.dart';
class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
ConversationBloc()
: _currentChatState = ChatState.gone,
_lastChangeTimestamp = 0,
super(ConversationState()) {
on<RequestedConversationEvent>(_onRequestedConversation);
on<MessageTextChangedEvent>(_onMessageTextChanged);
on<InitConversationEvent>(_onInit);
on<MessageSentEvent>(_onMessageSent);
on<MessageQuotedEvent>(_onMessageQuoted);
on<QuoteRemovedEvent>(_onQuoteRemoved);
on<JidBlockedEvent>(_onJidBlocked);
on<JidAddedEvent>(_onJidAdded);
on<CurrentConversationResetEvent>(_onCurrentConversationReset);
on<MessageAddedEvent>(_onMessageAdded);
on<MessageUpdatedEvent>(_onMessageUpdated);
on<ConversationUpdatedEvent>(_onConversationUpdated);
on<AppStateChanged>(_onAppStateChanged);
on<BackgroundChangedEvent>(_onBackgroundChanged);
on<ImagePickerRequestedEvent>(_onImagePickerRequested);
on<FilePickerRequestedEvent>(_onFilePickerRequested);
on<EmojiPickerToggledEvent>(_onEmojiPickerToggled);
on<OwnJidReceivedEvent>(_onOwnJidReceived);
on<OmemoSetEvent>(_onOmemoSet);
on<MessageRetractedEvent>(_onMessageRetracted);
}
/// The current chat state with the conversation partner
ChatState _currentChatState;
/// Timer to be able to send <paused /> notifications
Timer? _composeTimer;
/// The last time the text has been changed
int _lastChangeTimestamp;
void _setLastChangeTimestamp() {
_lastChangeTimestamp = DateTime.now().millisecondsSinceEpoch;
}
void _startComposeTimer() {
if (_composeTimer != null) return;
_composeTimer = Timer.periodic(
const Duration(seconds: 3),
(_) {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastChangeTimestamp >= 3000) {
// No change since 5 seconds
_updateChatState(ChatState.paused);
_stopComposeTimer();
}
}
);
}
void _stopComposeTimer() {
if (_composeTimer == null) return;
_composeTimer!.cancel();
_composeTimer = null;
}
bool _isSameConversation(String jid) => jid == state.conversation?.jid;
/// Returns true if [msg] is meant for the open conversation. False otherwise.
bool _isMessageForConversation(Message msg) => msg.conversationJid == state.conversation?.jid;
/// Updates the local chat state and sends a chat state notification to the conversation
/// partner.
void _updateChatState(ChatState s) {
if (s == _currentChatState) return;
_currentChatState = s;
MoxplatformPlugin.handler.getDataSender().sendData(
SendChatStateCommand(
state: s.toString().split('.').last,
jid: state.conversation!.jid,
),
awaitable: false,);
}
Future<void> _onInit(InitConversationEvent event, Emitter<ConversationState> emit) async {
emit(
state.copyWith(backgroundPath: event.backgroundPath),
);
}
Future<void> _onRequestedConversation(RequestedConversationEvent event, Emitter<ConversationState> emit) async {
final conversation = firstWhereOrNull(
GetIt.I.get<ConversationsBloc>().state.conversations,
(Conversation c) => c.jid == event.jid,
)!;
emit(
state.copyWith(
conversation: conversation,
quotedMessage: null,
),
);
_updateChatState(ChatState.active);
final navEvent = event.removeUntilConversations ? (
PushedNamedAndRemoveUntilEvent(
const NavigationDestination(conversationRoute),
ModalRoute.withName(conversationsRoute),
)
) : (
PushedNamedEvent(
const NavigationDestination(conversationRoute),
)
);
GetIt.I.get<NavigationBloc>().add(navEvent);
// ignore: cast_nullable_to_non_nullable
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
GetMessagesForJidCommand(
jid: event.jid,
),
) as events.MessagesResultEvent;
emit(state.copyWith(messages: result.messages));
await MoxplatformPlugin.handler.getDataSender().sendData(
SetOpenConversationCommand(jid: event.jid),
awaitable: false,
);
GetIt.I.get<SharedMediaBloc>().add(
SetSharedMedia(
conversation.title,
conversation.jid,
conversation.sharedMedia,
),
);
}
Future<void> _onMessageTextChanged(MessageTextChangedEvent event, Emitter<ConversationState> emit) async {
_setLastChangeTimestamp();
_startComposeTimer();
_updateChatState(ChatState.composing);
return emit(
state.copyWith(
messageText: event.value,
showSendButton: event.value.isNotEmpty,
),
);
}
Future<void> _onMessageSent(MessageSentEvent event, Emitter<ConversationState> emit) async {
// Set it but don't notify
_currentChatState = ChatState.active;
_stopComposeTimer();
// ignore: cast_nullable_to_non_nullable
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
SendMessageCommand(
recipients: [state.conversation!.jid],
body: state.messageText,
quotedMessage: state.quotedMessage,
chatState: chatStateToString(ChatState.active),
),
) as events.MessageAddedEvent;
emit(
state.copyWith(
messages: List<Message>.from(<Message>[ ...state.messages, result.message ]),
messageText: '',
quotedMessage: null,
showSendButton: false,
emojiPickerVisible: false,
),
);
}
Future<void> _onMessageQuoted(MessageQuotedEvent event, Emitter<ConversationState> emit) async {
// Ignore File Upload Notifications
if (event.message.isFileUploadNotification) return;
emit(
state.copyWith(
quotedMessage: event.message,
),
);
}
Future<void> _onQuoteRemoved(QuoteRemovedEvent event, Emitter<ConversationState> emit) async {
return emit(
state.copyWith(
quotedMessage: null,
),
);
}
Future<void> _onJidBlocked(JidBlockedEvent event, Emitter<ConversationState> emit) async {
// TODO(Unknown): Maybe have some state here
await MoxplatformPlugin.handler.getDataSender().sendData(
BlockJidCommand(jid: state.conversation!.jid),
);
}
Future<void> _onJidAdded(JidAddedEvent event, Emitter<ConversationState> emit) async {
// TODO(Unknown): Maybe have some state here
await MoxplatformPlugin.handler.getDataSender().sendData(
AddContactCommand(jid: state.conversation!.jid),
);
}
Future<void> _onCurrentConversationReset(CurrentConversationResetEvent event, Emitter<ConversationState> emit) async {
GetIt.I.get<SharedMediaBloc>().add(JidRemovedEvent());
_updateChatState(ChatState.gone);
await MoxplatformPlugin.handler.getDataSender().sendData(
SetOpenConversationCommand(),
awaitable: false,
);
}
Future<void> _onMessageAdded(MessageAddedEvent event, Emitter<ConversationState> emit) async {
if (!_isMessageForConversation(event.message)) return;
emit(
state.copyWith(
messages: List.from(<Message>[ ...state.messages, event.message ]),
),
);
}
Future<void> _onMessageUpdated(MessageUpdatedEvent event, Emitter<ConversationState> emit) async {
if (!_isMessageForConversation(event.message)) return;
// TODO(Unknown): Check if we are iterating the correct wa
// Small trick: The newer messages are much more likely to be updated than
// older messages.
/*
final messages = state.messages;
for (int i = messages.length - 1; i >= 0; i--) {
if (messages[i].id == event.message.id) {
print("Found message to update");
messages[i] = event.message;
break;
}
}
*/
// We don't have to re-sort the list here as timestamps never change
emit(
state.copyWith(
messages: List.from(
state.messages.map<dynamic>((Message m) {
if (m.id == event.message.id) return event.message;
return m;
}),
),
),
);
}
Future<void> _onConversationUpdated(ConversationUpdatedEvent event, Emitter<ConversationState> emit) async {
if (!_isSameConversation(event.conversation.jid)) return;
emit(state.copyWith(conversation: event.conversation));
}
Future<void> _onAppStateChanged(AppStateChanged event, Emitter<ConversationState> emit) async {
if (state.conversation == null) return;
if (event.open) {
_updateChatState(ChatState.active);
} else {
_stopComposeTimer();
_updateChatState(ChatState.gone);
}
}
Future<void> _onBackgroundChanged(BackgroundChangedEvent event, Emitter<ConversationState> emit) async {
return emit(state.copyWith(backgroundPath: event.backgroundPath));
}
Future<void> _onImagePickerRequested(ImagePickerRequestedEvent event, Emitter<ConversationState> emit) async {
GetIt.I.get<SendFilesBloc>().add(
SendFilesPageRequestedEvent([state.conversation!.jid], SendFilesType.image),
);
}
Future<void> _onFilePickerRequested(FilePickerRequestedEvent event, Emitter<ConversationState> emit) async {
GetIt.I.get<SendFilesBloc>().add(
SendFilesPageRequestedEvent([state.conversation!.jid], SendFilesType.generic),
);
}
Future<void> _onEmojiPickerToggled(EmojiPickerToggledEvent event, Emitter<ConversationState> emit) async {
final newState = !state.emojiPickerVisible;
emit(state.copyWith(emojiPickerVisible: newState));
if (event.handleKeyboard) {
if (newState) {
await SystemChannels.textInput.invokeMethod('TextInput.hide');
} else {
await SystemChannels.textInput.invokeMethod('TextInput.show');
}
}
}
Future<void> _onOwnJidReceived(OwnJidReceivedEvent event, Emitter<ConversationState> emit) async {
emit(state.copyWith(jid: event.jid));
}
Future<void> _onOmemoSet(OmemoSetEvent event, Emitter<ConversationState> emit) async {
emit(
state.copyWith(
conversation: state.conversation!.copyWith(
encrypted: event.enabled,
),
),
);
await MoxplatformPlugin.handler.getDataSender().sendData(
SetOmemoEnabledCommand(enabled: event.enabled, jid: state.conversation!.jid),
awaitable: false,
);
}
Future<void> _onMessageRetracted(MessageRetractedEvent event, Emitter<ConversationState> emit) async {
await MoxplatformPlugin.handler.getDataSender().sendData(
RetractMessageComment(
originId: event.id,
conversationJid: state.conversation!.jid,
),
awaitable: false,
);
}
}