Merge pull request 'Improve avatar and presence handling' (#285) from feat/better-avatar-handling into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/285
This commit is contained in:
commit
20489fbb25
@ -208,7 +208,7 @@ files:
|
|||||||
attributes:
|
attributes:
|
||||||
conversationJid: String
|
conversationJid: String
|
||||||
title: String
|
title: String
|
||||||
avatarUrl: String
|
avatarPath: String
|
||||||
- name: StickerPackImportSuccessEvent
|
- name: StickerPackImportSuccessEvent
|
||||||
extends: BackgroundEvent
|
extends: BackgroundEvent
|
||||||
implements:
|
implements:
|
||||||
@ -274,6 +274,13 @@ files:
|
|||||||
reactions:
|
reactions:
|
||||||
type: List<ReactionGroup>
|
type: List<ReactionGroup>
|
||||||
deserialise: true
|
deserialise: true
|
||||||
|
# Triggered when the stream negotiations have been completed
|
||||||
|
- name: StreamNegotiationsCompletedEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
resumed: bool
|
||||||
generate_builder: true
|
generate_builder: true
|
||||||
builder_name: "Event"
|
builder_name: "Event"
|
||||||
builder_baseclass: "BackgroundEvent"
|
builder_baseclass: "BackgroundEvent"
|
||||||
@ -576,6 +583,20 @@ files:
|
|||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
messageId: int
|
messageId: int
|
||||||
|
- name: RequestAvatarForJidCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
jid: String
|
||||||
|
hash: String?
|
||||||
|
ownAvatar: bool
|
||||||
|
- name: DebugCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
id: int
|
||||||
generate_builder: true
|
generate_builder: true
|
||||||
# get${builder_Name}FromJson
|
# get${builder_Name}FromJson
|
||||||
builder_name: "Command"
|
builder_name: "Command"
|
||||||
|
@ -63,6 +63,7 @@ import 'package:moxxyv2/ui/pages/share_selection.dart';
|
|||||||
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
|
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
|
||||||
import 'package:moxxyv2/ui/pages/sticker_pack.dart';
|
import 'package:moxxyv2/ui/pages/sticker_pack.dart';
|
||||||
import 'package:moxxyv2/ui/pages/util/qrcode.dart';
|
import 'package:moxxyv2/ui/pages/util/qrcode.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/avatars.dart';
|
||||||
import 'package:moxxyv2/ui/service/data.dart';
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
import 'package:moxxyv2/ui/service/progress.dart';
|
import 'package:moxxyv2/ui/service/progress.dart';
|
||||||
import 'package:moxxyv2/ui/service/sharing.dart';
|
import 'package:moxxyv2/ui/service/sharing.dart';
|
||||||
@ -83,6 +84,7 @@ void setupLogging() {
|
|||||||
Future<void> setupUIServices() async {
|
Future<void> setupUIServices() async {
|
||||||
GetIt.I.registerSingleton<UIProgressService>(UIProgressService());
|
GetIt.I.registerSingleton<UIProgressService>(UIProgressService());
|
||||||
GetIt.I.registerSingleton<UIDataService>(UIDataService());
|
GetIt.I.registerSingleton<UIDataService>(UIDataService());
|
||||||
|
GetIt.I.registerSingleton<UIAvatarsService>(UIAvatarsService());
|
||||||
GetIt.I.registerSingleton<UISharingService>(UISharingService());
|
GetIt.I.registerSingleton<UISharingService>(UISharingService());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:cryptography/cryptography.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:hex/hex.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:moxxyv2/service/conversation.dart';
|
import 'package:moxxyv2/service/conversation.dart';
|
||||||
@ -14,60 +12,100 @@ import 'package:moxxyv2/shared/avatar.dart';
|
|||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
|
|
||||||
/// Removes line breaks and spaces from [original]. This might happen when we request the
|
|
||||||
/// avatar data. Returns the cleaned version.
|
|
||||||
String _cleanBase64String(String original) {
|
|
||||||
var ret = original;
|
|
||||||
for (final char in ['\n', ' ']) {
|
|
||||||
ret = ret.replaceAll(char, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AvatarData {
|
|
||||||
const _AvatarData(this.data, this.id);
|
|
||||||
final List<int> data;
|
|
||||||
final String id;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AvatarService {
|
class AvatarService {
|
||||||
final Logger _log = Logger('AvatarService');
|
final Logger _log = Logger('AvatarService');
|
||||||
|
|
||||||
Future<void> handleAvatarUpdate(AvatarUpdatedEvent event) async {
|
/// List of JIDs for which we have already requested the avatar in the current stream.
|
||||||
await updateAvatarForJid(
|
final List<JID> _requestedInStream = [];
|
||||||
event.jid,
|
|
||||||
event.hash,
|
void resetCache() {
|
||||||
base64Decode(_cleanBase64String(event.base64)),
|
_requestedInStream.clear();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateAvatarForJid(
|
Future<bool> _fetchAvatarForJid(JID jid, String hash) async {
|
||||||
String jid,
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
final am = conn.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
||||||
|
final rawAvatar = await am.getUserAvatar(jid);
|
||||||
|
if (rawAvatar.isType<AvatarError>()) {
|
||||||
|
_log.warning('Failed to request avatar for $jid');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final avatar = rawAvatar.get<UserAvatarData>();
|
||||||
|
await _updateAvatarForJid(
|
||||||
|
jid,
|
||||||
|
avatar.hash,
|
||||||
|
avatar.data,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests the avatar for [jid]. [oldHash], if given, is the last SHA-1 hash of the known avatar.
|
||||||
|
/// If the avatar for [jid] has already been requested in this stream session, does nothing. Otherwise,
|
||||||
|
/// requests the XEP-0084 metadata and queries the new avatar only if the queried SHA-1 != [oldHash].
|
||||||
|
///
|
||||||
|
/// Returns true, if everything went okay. Returns false if an error occurred.
|
||||||
|
Future<bool> requestAvatar(JID jid, String? oldHash) async {
|
||||||
|
if (_requestedInStream.contains(jid)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestedInStream.add(jid);
|
||||||
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
final am = conn.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
||||||
|
final rawId = await am.getAvatarId(jid);
|
||||||
|
|
||||||
|
if (rawId.isType<AvatarError>()) {
|
||||||
|
_log.finest(
|
||||||
|
'Failed to get avatar metadata for $jid using XEP-0084: ${rawId.get<AvatarError>()}',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final id = rawId.get<String>();
|
||||||
|
if (id == oldHash) {
|
||||||
|
_log.finest('Not fetching avatar for $jid since the hashes are equal');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _fetchAvatarForJid(jid, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleAvatarUpdate(UserAvatarUpdatedEvent event) async {
|
||||||
|
if (event.metadata.isEmpty) return;
|
||||||
|
|
||||||
|
// TODO(Unknown): Maybe make a better decision?
|
||||||
|
await _fetchAvatarForJid(event.jid, event.metadata.first.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the avatar path and hash for the conversation and/or roster item with jid [JID].
|
||||||
|
/// [hash] is the new hash of the avatar. [data] is the raw avatar data.
|
||||||
|
Future<void> _updateAvatarForJid(
|
||||||
|
JID jid,
|
||||||
String hash,
|
String hash,
|
||||||
List<int> data,
|
List<int> data,
|
||||||
) async {
|
) async {
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
final rs = GetIt.I.get<RosterService>();
|
final rs = GetIt.I.get<RosterService>();
|
||||||
final originalConversation = await cs.getConversationByJid(jid);
|
final originalConversation = await cs.getConversationByJid(jid.toString());
|
||||||
final originalRoster = await rs.getRosterItemByJid(jid);
|
final originalRoster = await rs.getRosterItemByJid(jid.toString());
|
||||||
|
|
||||||
if (originalConversation == null && originalRoster == null) return;
|
if (originalConversation == null && originalRoster == null) return;
|
||||||
|
|
||||||
final avatarPath = await saveAvatarInCache(
|
final avatarPath = await saveAvatarInCache(
|
||||||
data,
|
data,
|
||||||
hash,
|
hash,
|
||||||
jid,
|
jid.toString(),
|
||||||
(originalConversation?.avatarUrl ?? originalRoster?.avatarUrl)!,
|
(originalConversation?.avatarPath ?? originalRoster?.avatarPath)!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (originalConversation != null) {
|
if (originalConversation != null) {
|
||||||
final conversation = await cs.createOrUpdateConversation(
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
jid,
|
jid.toString(),
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
jid,
|
jid.toString(),
|
||||||
avatarUrl: avatarPath,
|
avatarPath: avatarPath,
|
||||||
|
avatarHash: hash,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -81,7 +119,7 @@ class AvatarService {
|
|||||||
if (originalRoster != null) {
|
if (originalRoster != null) {
|
||||||
final roster = await rs.updateRosterItem(
|
final roster = await rs.updateRosterItem(
|
||||||
originalRoster.id,
|
originalRoster.id,
|
||||||
avatarUrl: avatarPath,
|
avatarPath: avatarPath,
|
||||||
avatarHash: hash,
|
avatarHash: hash,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -89,80 +127,6 @@ class AvatarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<_AvatarData?> _handleUserAvatar(String jid, String oldHash) async {
|
|
||||||
final am = GetIt.I
|
|
||||||
.get<XmppConnection>()
|
|
||||||
.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
|
||||||
final idResult = await am.getAvatarId(JID.fromString(jid));
|
|
||||||
if (idResult.isType<AvatarError>()) {
|
|
||||||
_log.warning('Failed to get avatar id via XEP-0084 for $jid');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final id = idResult.get<String>();
|
|
||||||
if (id == oldHash) return null;
|
|
||||||
|
|
||||||
final avatarResult = await am.getUserAvatar(jid);
|
|
||||||
if (avatarResult.isType<AvatarError>()) {
|
|
||||||
_log.warning('Failed to get avatar data via XEP-0084 for $jid');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final avatar = avatarResult.get<UserAvatar>();
|
|
||||||
|
|
||||||
return _AvatarData(
|
|
||||||
base64Decode(_cleanBase64String(avatar.base64)),
|
|
||||||
avatar.hash,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<_AvatarData?> _handleVcardAvatar(String jid, String oldHash) async {
|
|
||||||
// Query the vCard
|
|
||||||
final vm = GetIt.I
|
|
||||||
.get<XmppConnection>()
|
|
||||||
.getManagerById<VCardManager>(vcardManager)!;
|
|
||||||
final vcardResult = await vm.requestVCard(jid);
|
|
||||||
if (vcardResult.isType<VCardError>()) return null;
|
|
||||||
|
|
||||||
final binval = vcardResult.get<VCard>().photo?.binval;
|
|
||||||
if (binval == null) return null;
|
|
||||||
|
|
||||||
final data = base64Decode(_cleanBase64String(binval));
|
|
||||||
final rawHash = await Sha1().hash(data);
|
|
||||||
final hash = HEX.encode(rawHash.bytes);
|
|
||||||
|
|
||||||
vm.setLastHash(jid, hash);
|
|
||||||
|
|
||||||
return _AvatarData(
|
|
||||||
data,
|
|
||||||
hash,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchAndUpdateAvatarForJid(String jid, String oldHash) async {
|
|
||||||
_AvatarData? data;
|
|
||||||
data ??= await _handleUserAvatar(jid, oldHash);
|
|
||||||
data ??= await _handleVcardAvatar(jid, oldHash);
|
|
||||||
|
|
||||||
if (data != null) {
|
|
||||||
await updateAvatarForJid(jid, data.id, data.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> subscribeJid(String jid) async {
|
|
||||||
return (await GetIt.I
|
|
||||||
.get<XmppConnection>()
|
|
||||||
.getManagerById<UserAvatarManager>(userAvatarManager)!
|
|
||||||
.subscribe(jid))
|
|
||||||
.isType<bool>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> unsubscribeJid(String jid) async {
|
|
||||||
return (await GetIt.I
|
|
||||||
.get<XmppConnection>()
|
|
||||||
.getManagerById<UserAvatarManager>(userAvatarManager)!
|
|
||||||
.unsubscribe(jid))
|
|
||||||
.isType<bool>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publishes the data at [path] as an avatar with PubSub ID
|
/// Publishes the data at [path] as an avatar with PubSub ID
|
||||||
/// [hash]. [hash] must be the hex-encoded version of the SHA-1 hash
|
/// [hash]. [hash] must be the hex-encoded version of the SHA-1 hash
|
||||||
/// of the avatar data.
|
/// of the avatar data.
|
||||||
@ -201,6 +165,7 @@ class AvatarService {
|
|||||||
imageSize.height.toInt(),
|
imageSize.height.toInt(),
|
||||||
// TODO(PapaTutuWawa): Maybe do a check here
|
// TODO(PapaTutuWawa): Maybe do a check here
|
||||||
'image/png',
|
'image/png',
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
public,
|
public,
|
||||||
);
|
);
|
||||||
@ -213,38 +178,44 @@ class AvatarService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [requestAvatar], but fetches and processes the avatar for our own account.
|
||||||
Future<void> requestOwnAvatar() async {
|
Future<void> requestOwnAvatar() async {
|
||||||
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
|
final state = await xss.getXmppState();
|
||||||
|
final jid = JID.fromString(state.jid!);
|
||||||
|
|
||||||
|
if (_requestedInStream.contains(jid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_requestedInStream.add(jid);
|
||||||
|
|
||||||
final am = GetIt.I
|
final am = GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
||||||
final xss = GetIt.I.get<XmppStateService>();
|
final rawId = await am.getAvatarId(jid);
|
||||||
final state = await xss.getXmppState();
|
if (rawId.isType<AvatarError>()) {
|
||||||
final jid = state.jid!;
|
_log.finest(
|
||||||
final idResult = await am.getAvatarId(JID.fromString(jid));
|
'Failed to get avatar metadata for $jid using XEP-0084: ${rawId.get<AvatarError>()}',
|
||||||
if (idResult.isType<AvatarError>()) {
|
);
|
||||||
_log.info('Error while getting latest avatar id for own avatar');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final id = idResult.get<String>();
|
final id = rawId.get<String>();
|
||||||
|
|
||||||
if (id == state.avatarHash) return;
|
if (id == state.avatarHash) {
|
||||||
|
_log.finest('Not fetching avatar for $jid since the hashes are equal');
|
||||||
_log.info(
|
|
||||||
'Mismatch between saved avatar data and server-side avatar data about ourself',
|
|
||||||
);
|
|
||||||
final avatarDataResult = await am.getUserAvatar(jid);
|
|
||||||
if (avatarDataResult.isType<AvatarError>()) {
|
|
||||||
_log.severe('Failed to fetch our avatar');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final avatarData = avatarDataResult.get<UserAvatar>();
|
|
||||||
|
|
||||||
_log.info('Received data for our own avatar');
|
|
||||||
|
|
||||||
|
final rawAvatar = await am.getUserAvatar(jid);
|
||||||
|
if (rawAvatar.isType<AvatarError>()) {
|
||||||
|
_log.warning('Failed to request avatar for $jid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final avatarData = rawAvatar.get<UserAvatarData>();
|
||||||
final avatarPath = await saveAvatarInCache(
|
final avatarPath = await saveAvatarInCache(
|
||||||
base64Decode(_cleanBase64String(avatarData.base64)),
|
avatarData.data,
|
||||||
avatarData.hash,
|
avatarData.hash,
|
||||||
jid,
|
jid.toString(),
|
||||||
state.avatarUrl,
|
state.avatarUrl,
|
||||||
);
|
);
|
||||||
await xss.modifyXmppState(
|
await xss.modifyXmppState(
|
||||||
|
@ -87,8 +87,7 @@ class ConversationService {
|
|||||||
tmp.add(
|
tmp.add(
|
||||||
Conversation.fromDatabaseJson(
|
Conversation.fromDatabaseJson(
|
||||||
c,
|
c,
|
||||||
rosterItem != null && !rosterItem.pseudoRosterItem,
|
rosterItem?.showAddToRosterButton ?? true,
|
||||||
rosterItem?.subscription ?? 'none',
|
|
||||||
lastMessage,
|
lastMessage,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -136,7 +135,8 @@ class ConversationService {
|
|||||||
Message? lastMessage,
|
Message? lastMessage,
|
||||||
bool? open,
|
bool? open,
|
||||||
int? unreadCounter,
|
int? unreadCounter,
|
||||||
String? avatarUrl,
|
String? avatarPath,
|
||||||
|
Object? avatarHash = notSpecified,
|
||||||
ChatState? chatState,
|
ChatState? chatState,
|
||||||
bool? muted,
|
bool? muted,
|
||||||
bool? encrypted,
|
bool? encrypted,
|
||||||
@ -160,8 +160,11 @@ class ConversationService {
|
|||||||
if (unreadCounter != null) {
|
if (unreadCounter != null) {
|
||||||
c['unreadCounter'] = unreadCounter;
|
c['unreadCounter'] = unreadCounter;
|
||||||
}
|
}
|
||||||
if (avatarUrl != null) {
|
if (avatarPath != null) {
|
||||||
c['avatarUrl'] = avatarUrl;
|
c['avatarPath'] = avatarPath;
|
||||||
|
}
|
||||||
|
if (avatarHash != notSpecified) {
|
||||||
|
c['avatarHash'] = avatarHash as String?;
|
||||||
}
|
}
|
||||||
if (muted != null) {
|
if (muted != null) {
|
||||||
c['muted'] = boolToInt(muted);
|
c['muted'] = boolToInt(muted);
|
||||||
@ -191,8 +194,7 @@ class ConversationService {
|
|||||||
await GetIt.I.get<RosterService>().getRosterItemByJid(jid);
|
await GetIt.I.get<RosterService>().getRosterItemByJid(jid);
|
||||||
var newConversation = Conversation.fromDatabaseJson(
|
var newConversation = Conversation.fromDatabaseJson(
|
||||||
result,
|
result,
|
||||||
rosterItem != null,
|
rosterItem?.showAddToRosterButton ?? true,
|
||||||
rosterItem?.subscription ?? 'none',
|
|
||||||
lastMessage,
|
lastMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -215,7 +217,7 @@ class ConversationService {
|
|||||||
String title,
|
String title,
|
||||||
Message? lastMessage,
|
Message? lastMessage,
|
||||||
ConversationType type,
|
ConversationType type,
|
||||||
String avatarUrl,
|
String avatarPath,
|
||||||
String jid,
|
String jid,
|
||||||
int unreadCounter,
|
int unreadCounter,
|
||||||
int lastChangeTimestamp,
|
int lastChangeTimestamp,
|
||||||
@ -231,14 +233,14 @@ class ConversationService {
|
|||||||
final newConversation = Conversation(
|
final newConversation = Conversation(
|
||||||
title,
|
title,
|
||||||
lastMessage,
|
lastMessage,
|
||||||
avatarUrl,
|
avatarPath,
|
||||||
|
null,
|
||||||
jid,
|
jid,
|
||||||
unreadCounter,
|
unreadCounter,
|
||||||
type,
|
type,
|
||||||
lastChangeTimestamp,
|
lastChangeTimestamp,
|
||||||
open,
|
open,
|
||||||
rosterItem != null && !rosterItem.pseudoRosterItem,
|
rosterItem?.showAddToRosterButton ?? true,
|
||||||
rosterItem?.subscription ?? 'none',
|
|
||||||
muted,
|
muted,
|
||||||
encrypted,
|
encrypted,
|
||||||
ChatState.gone,
|
ChatState.gone,
|
||||||
|
@ -41,6 +41,8 @@ import 'package:moxxyv2/service/database/migrations/0002_reactions.dart';
|
|||||||
import 'package:moxxyv2/service/database/migrations/0002_reactions_2.dart';
|
import 'package:moxxyv2/service/database/migrations/0002_reactions_2.dart';
|
||||||
import 'package:moxxyv2/service/database/migrations/0002_shared_media.dart';
|
import 'package:moxxyv2/service/database/migrations/0002_shared_media.dart';
|
||||||
import 'package:moxxyv2/service/database/migrations/0002_sticker_metadata.dart';
|
import 'package:moxxyv2/service/database/migrations/0002_sticker_metadata.dart';
|
||||||
|
import 'package:moxxyv2/service/database/migrations/0003_avatar_hashes.dart';
|
||||||
|
import 'package:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:random_string/random_string.dart';
|
import 'package:random_string/random_string.dart';
|
||||||
// ignore: implementation_imports
|
// ignore: implementation_imports
|
||||||
@ -144,6 +146,8 @@ const List<DatabaseMigration<Database>> migrations = [
|
|||||||
DatabaseMigration(35, upgradeFromV34ToV35),
|
DatabaseMigration(35, upgradeFromV34ToV35),
|
||||||
DatabaseMigration(36, upgradeFromV35ToV36),
|
DatabaseMigration(36, upgradeFromV35ToV36),
|
||||||
DatabaseMigration(37, upgradeFromV36ToV37),
|
DatabaseMigration(37, upgradeFromV36ToV37),
|
||||||
|
DatabaseMigration(38, upgradeFromV37ToV38),
|
||||||
|
DatabaseMigration(39, upgradeFromV38ToV39),
|
||||||
];
|
];
|
||||||
|
|
||||||
class DatabaseService {
|
class DatabaseService {
|
||||||
@ -179,10 +183,23 @@ class DatabaseService {
|
|||||||
_log.finest('Key generation done...');
|
_log.finest('Key generation done...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Just some sanity checks
|
||||||
|
final version = migrations.last.version;
|
||||||
|
assert(
|
||||||
|
migrations.every((migration) => migration.version <= version),
|
||||||
|
"Every migration's version must be smaller or equal to the last version",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
migrations
|
||||||
|
.sublist(0, migrations.length - 1)
|
||||||
|
.every((migration) => migration.version < version),
|
||||||
|
'The last migration must have the largest version',
|
||||||
|
);
|
||||||
|
|
||||||
database = await openDatabase(
|
database = await openDatabase(
|
||||||
dbPath,
|
dbPath,
|
||||||
password: key,
|
password: key,
|
||||||
version: 37,
|
version: version,
|
||||||
onCreate: createDatabase,
|
onCreate: createDatabase,
|
||||||
onConfigure: (db) async {
|
onConfigure: (db) async {
|
||||||
// In order to do schema changes during database upgrades, we disable foreign
|
// In order to do schema changes during database upgrades, we disable foreign
|
||||||
|
13
lib/service/database/migrations/0003_avatar_hashes.dart
Normal file
13
lib/service/database/migrations/0003_avatar_hashes.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV37ToV38(Database db) async {
|
||||||
|
await db
|
||||||
|
.execute('ALTER TABLE $conversationsTable ADD COLUMN avatarHash TEXT');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable RENAME COLUMN avatarUrl TO avatarPath',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $rosterTable RENAME COLUMN avatarUrl TO avatarPath',
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV38ToV39(Database db) async {
|
||||||
|
await db.execute('DROP TABLE $subscriptionsTable');
|
||||||
|
}
|
@ -25,10 +25,10 @@ import 'package:moxxyv2/service/reactions.dart';
|
|||||||
import 'package:moxxyv2/service/roster.dart';
|
import 'package:moxxyv2/service/roster.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/service/stickers.dart';
|
import 'package:moxxyv2/service/stickers.dart';
|
||||||
import 'package:moxxyv2/service/subscription.dart';
|
|
||||||
import 'package:moxxyv2/service/xmpp.dart';
|
import 'package:moxxyv2/service/xmpp.dart';
|
||||||
import 'package:moxxyv2/service/xmpp_state.dart';
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
|
import 'package:moxxyv2/shared/debug.dart' as debug;
|
||||||
import 'package:moxxyv2/shared/eventhandler.dart';
|
import 'package:moxxyv2/shared/eventhandler.dart';
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
@ -100,6 +100,8 @@ void setupBackgroundEventHandler() {
|
|||||||
EventTypeMatcher<GetPagedMessagesCommand>(performGetPagedMessages),
|
EventTypeMatcher<GetPagedMessagesCommand>(performGetPagedMessages),
|
||||||
EventTypeMatcher<GetPagedSharedMediaCommand>(performGetPagedSharedMedia),
|
EventTypeMatcher<GetPagedSharedMediaCommand>(performGetPagedSharedMedia),
|
||||||
EventTypeMatcher<GetReactionsForMessageCommand>(performGetReactions),
|
EventTypeMatcher<GetReactionsForMessageCommand>(performGetReactions),
|
||||||
|
EventTypeMatcher<RequestAvatarForJidCommand>(performRequestAvatarForJid),
|
||||||
|
EventTypeMatcher<DebugCommand>(performDebugCommand),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
GetIt.I.registerSingleton<EventHandler>(handler);
|
GetIt.I.registerSingleton<EventHandler>(handler);
|
||||||
@ -318,7 +320,7 @@ Future<void> performSendMessage(
|
|||||||
command.editSid!,
|
command.editSid!,
|
||||||
command.recipients.first,
|
command.recipients.first,
|
||||||
command.chatState.isNotEmpty
|
command.chatState.isNotEmpty
|
||||||
? chatStateFromString(command.chatState)
|
? ChatState.fromName(command.chatState)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -328,7 +330,7 @@ Future<void> performSendMessage(
|
|||||||
body: command.body,
|
body: command.body,
|
||||||
recipients: command.recipients,
|
recipients: command.recipients,
|
||||||
chatState: command.chatState.isNotEmpty
|
chatState: command.chatState.isNotEmpty
|
||||||
? chatStateFromString(command.chatState)
|
? ChatState.fromName(command.chatState)
|
||||||
: null,
|
: null,
|
||||||
quotedMessage: command.quotedMessage,
|
quotedMessage: command.quotedMessage,
|
||||||
currentConversationJid: command.currentConversationJid,
|
currentConversationJid: command.currentConversationJid,
|
||||||
@ -505,30 +507,39 @@ Future<void> performAddContact(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Manage subscription requests
|
|
||||||
final srs = GetIt.I.get<SubscriptionRequestService>();
|
|
||||||
final hasSubscriptionRequest = await srs.hasPendingSubscriptionRequest(jid);
|
|
||||||
if (hasSubscriptionRequest) {
|
|
||||||
await srs.acceptSubscriptionRequest(jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to roster, if needed
|
// Add to roster, if needed
|
||||||
final item = await roster.getRosterItemByJid(jid);
|
final item = await roster.getRosterItemByJid(jid);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
if (item.subscription != 'from' && item.subscription != 'both') {
|
GetIt.I.get<Logger>().finest(
|
||||||
GetIt.I.get<Logger>().finest(
|
'Roster item for $jid has subscription "${item.subscription}" with ask "${item.ask}"',
|
||||||
'Roster item already exists with no presence subscription from them. Sending subscription request',
|
);
|
||||||
);
|
|
||||||
srs.sendSubscriptionRequest(jid);
|
// Nothing more to do
|
||||||
|
if (item.subscription == 'both') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pm = GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<PresenceManager>(presenceManager)!;
|
||||||
|
switch (item.subscription) {
|
||||||
|
case 'both':
|
||||||
|
return;
|
||||||
|
case 'none':
|
||||||
|
case 'from':
|
||||||
|
if (item.ask != 'subscribe') {
|
||||||
|
// Try to move from "from"/"none" to "both", by going over "From + Pending Out"
|
||||||
|
await pm.requestSubscription(JID.fromString(item.jid));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'to':
|
||||||
|
// Move from "to" to "both"
|
||||||
|
await pm.acceptSubscriptionRequest(JID.fromString(item.jid));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await roster.addToRosterWrapper('', '', jid, jid.split('@')[0]);
|
await roster.addToRosterWrapper('', '', jid, jid.split('@')[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to figure out an avatar
|
|
||||||
// TODO(Unknown): Don't do that here. Do it more intelligently.
|
|
||||||
await GetIt.I.get<AvatarService>().subscribeJid(jid);
|
|
||||||
await GetIt.I.get<AvatarService>().fetchAndUpdateAvatarForJid(jid, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> performRemoveContact(
|
Future<void> performRemoveContact(
|
||||||
@ -547,7 +558,7 @@ Future<void> performRemoveContact(
|
|||||||
sendEvent(
|
sendEvent(
|
||||||
ConversationUpdatedEvent(
|
ConversationUpdatedEvent(
|
||||||
conversation: conversation.copyWith(
|
conversation: conversation.copyWith(
|
||||||
inRoster: false,
|
showAddToRoster: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -615,23 +626,32 @@ Future<void> performSetShareOnlineStatus(
|
|||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
final rs = GetIt.I.get<RosterService>();
|
final rs = GetIt.I.get<RosterService>();
|
||||||
final srs = GetIt.I.get<SubscriptionRequestService>();
|
|
||||||
final item = await rs.getRosterItemByJid(command.jid);
|
final item = await rs.getRosterItemByJid(command.jid);
|
||||||
|
|
||||||
// TODO(Unknown): Maybe log
|
// TODO(Unknown): Maybe log
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
|
|
||||||
|
final jid = JID.fromString(command.jid);
|
||||||
|
final pm = GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<PresenceManager>(presenceManager)!;
|
||||||
if (command.share) {
|
if (command.share) {
|
||||||
if (item.ask == 'subscribe') {
|
switch (item.subscription) {
|
||||||
await srs.acceptSubscriptionRequest(command.jid);
|
case 'to':
|
||||||
} else {
|
await pm.acceptSubscriptionRequest(jid);
|
||||||
srs.sendSubscriptionRequest(command.jid);
|
break;
|
||||||
|
case 'none':
|
||||||
|
case 'from':
|
||||||
|
await pm.requestSubscription(jid);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (item.ask == 'subscribe') {
|
switch (item.subscription) {
|
||||||
await srs.rejectSubscriptionRequest(command.jid);
|
case 'both':
|
||||||
} else {
|
case 'from':
|
||||||
srs.sendUnsubscriptionRequest(command.jid);
|
case 'to':
|
||||||
|
await pm.unsubscribe(jid);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -673,9 +693,9 @@ Future<void> performSendChatState(
|
|||||||
final conn = GetIt.I.get<XmppConnection>();
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
|
||||||
if (command.jid != '') {
|
if (command.jid != '') {
|
||||||
conn
|
await conn
|
||||||
.getManagerById<ChatStateManager>(chatStateManager)!
|
.getManagerById<ChatStateManager>(chatStateManager)!
|
||||||
.sendChatState(chatStateFromString(command.state), command.jid);
|
.sendChatState(ChatState.fromName(command.state), command.jid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -864,17 +884,14 @@ Future<void> performMessageRetraction(
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if (command.conversationJid != '') {
|
if (command.conversationJid != '') {
|
||||||
(GetIt.I
|
final manager = GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getManagerById<MessageManager>(messageManager)!)
|
.getManagerById<MessageManager>(messageManager)!;
|
||||||
.sendMessage(
|
await manager.sendMessage(
|
||||||
MessageDetails(
|
JID.fromString(command.conversationJid),
|
||||||
to: command.conversationJid,
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
messageRetraction: MessageRetractionData(
|
MessageRetractionData(command.originId, t.messages.retractedFallback),
|
||||||
command.originId,
|
]),
|
||||||
t.messages.retractedFallback,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -956,24 +973,25 @@ Future<void> performAddMessageReaction(
|
|||||||
final jid = (await GetIt.I.get<XmppStateService>().getXmppState()).jid!;
|
final jid = (await GetIt.I.get<XmppStateService>().getXmppState()).jid!;
|
||||||
|
|
||||||
// Send the reaction
|
// Send the reaction
|
||||||
GetIt.I
|
final manager = GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getManagerById<MessageManager>(messageManager)!
|
.getManagerById<MessageManager>(messageManager)!;
|
||||||
.sendMessage(
|
await manager.sendMessage(
|
||||||
MessageDetails(
|
JID.fromString(command.conversationJid),
|
||||||
to: command.conversationJid,
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
messageReactions: MessageReactions(
|
MessageReactionsData(
|
||||||
msg.originId ?? msg.sid,
|
msg.originId ?? msg.sid,
|
||||||
await rs.getReactionsForMessageByJid(
|
await rs.getReactionsForMessageByJid(
|
||||||
command.messageId,
|
command.messageId,
|
||||||
jid,
|
jid,
|
||||||
),
|
|
||||||
),
|
|
||||||
requestChatMarkers: false,
|
|
||||||
messageProcessingHints:
|
|
||||||
!msg.containsNoStore ? [MessageProcessingHint.store] : null,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
const MarkableData(false),
|
||||||
|
MessageProcessingHintData([
|
||||||
|
if (!msg.containsNoStore) MessageProcessingHint.store,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -995,24 +1013,25 @@ Future<void> performRemoveMessageReaction(
|
|||||||
final jid = (await GetIt.I.get<XmppStateService>().getXmppState()).jid!;
|
final jid = (await GetIt.I.get<XmppStateService>().getXmppState()).jid!;
|
||||||
|
|
||||||
// Send the reaction
|
// Send the reaction
|
||||||
GetIt.I
|
final manager = GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getManagerById<MessageManager>(messageManager)!
|
.getManagerById<MessageManager>(messageManager)!;
|
||||||
.sendMessage(
|
await manager.sendMessage(
|
||||||
MessageDetails(
|
JID.fromString(command.conversationJid),
|
||||||
to: command.conversationJid,
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
messageReactions: MessageReactions(
|
MessageReactionsData(
|
||||||
msg.originId ?? msg.sid,
|
msg.originId ?? msg.sid,
|
||||||
await rs.getReactionsForMessageByJid(
|
await rs.getReactionsForMessageByJid(
|
||||||
command.messageId,
|
command.messageId,
|
||||||
jid,
|
jid,
|
||||||
),
|
|
||||||
),
|
|
||||||
requestChatMarkers: false,
|
|
||||||
messageProcessingHints:
|
|
||||||
!msg.containsNoStore ? [MessageProcessingHint.store] : null,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
const MarkableData(false),
|
||||||
|
MessageProcessingHintData([
|
||||||
|
if (!msg.containsNoStore) MessageProcessingHint.store,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1248,3 +1267,46 @@ Future<void> performGetReactions(
|
|||||||
id: id,
|
id: id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> performRequestAvatarForJid(
|
||||||
|
RequestAvatarForJidCommand command, {
|
||||||
|
dynamic extra,
|
||||||
|
}) async {
|
||||||
|
final as = GetIt.I.get<AvatarService>();
|
||||||
|
Future<void> future;
|
||||||
|
if (command.ownAvatar) {
|
||||||
|
future = as.requestOwnAvatar();
|
||||||
|
} else {
|
||||||
|
future = as.requestAvatar(
|
||||||
|
JID.fromString(command.jid),
|
||||||
|
command.hash,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> performDebugCommand(
|
||||||
|
DebugCommand command, {
|
||||||
|
dynamic extra,
|
||||||
|
}) async {
|
||||||
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
|
||||||
|
if (command.id == debug.DebugCommand.clearStreamResumption.id) {
|
||||||
|
// Disconnect
|
||||||
|
await conn.disconnect();
|
||||||
|
|
||||||
|
// Reset stream management
|
||||||
|
await conn.getManagerById<StreamManagementManager>(smManager)!.resetState();
|
||||||
|
|
||||||
|
// Reconnect
|
||||||
|
await conn.connect(
|
||||||
|
shouldReconnect: true,
|
||||||
|
waitForConnection: true,
|
||||||
|
);
|
||||||
|
} else if (command.id == debug.DebugCommand.requestRoster.id) {
|
||||||
|
await conn
|
||||||
|
.getManagerById<RosterManager>(rosterManager)!
|
||||||
|
.requestRoster(useRosterVersion: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -124,11 +124,7 @@ String getUnrecoverableErrorString(NonRecoverableErrorEvent event) {
|
|||||||
/// This information is complemented either the srcUrl or – if unavailable –
|
/// This information is complemented either the srcUrl or – if unavailable –
|
||||||
/// by the body of the quoted message. For non-media messages, we always use
|
/// by the body of the quoted message. For non-media messages, we always use
|
||||||
/// the body as fallback.
|
/// the body as fallback.
|
||||||
String? createFallbackBodyForQuotedMessage(Message? quotedMessage) {
|
String createFallbackBodyForQuotedMessage(Message quotedMessage) {
|
||||||
if (quotedMessage == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quotedMessage.isMedia) {
|
if (quotedMessage.isMedia) {
|
||||||
// Create formatted size string, if size is stored
|
// Create formatted size string, if size is stored
|
||||||
String quoteMessageSize;
|
String quoteMessageSize;
|
||||||
|
@ -338,14 +338,13 @@ class HttpFileTransferService {
|
|||||||
sendEvent(MessageUpdatedEvent(message: msg));
|
sendEvent(MessageUpdatedEvent(message: msg));
|
||||||
|
|
||||||
// Send the message to the recipient
|
// Send the message to the recipient
|
||||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
await conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
||||||
MessageDetails(
|
JID.fromString(recipient),
|
||||||
to: recipient,
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
body: slot.getUrl,
|
MessageBodyData(slot.getUrl),
|
||||||
requestDeliveryReceipt: true,
|
const MessageDeliveryReceiptData(true),
|
||||||
id: msg.sid,
|
StableIdData(msg.originId, null),
|
||||||
originId: msg.originId,
|
StatelessFileSharingData(
|
||||||
sfs: StatelessFileSharingData(
|
|
||||||
FileMetadataData(
|
FileMetadataData(
|
||||||
mediaType: job.mime,
|
mediaType: job.mime,
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
@ -353,11 +352,12 @@ class HttpFileTransferService {
|
|||||||
thumbnails: job.thumbnails,
|
thumbnails: job.thumbnails,
|
||||||
hashes: plaintextHashes,
|
hashes: plaintextHashes,
|
||||||
),
|
),
|
||||||
<StatelessFileSharingSource>[source],
|
[source],
|
||||||
|
includeOOBFallback: true,
|
||||||
),
|
),
|
||||||
shouldEncrypt: job.encryptMap[recipient]!,
|
FileUploadNotificationReplacementData(oldSid),
|
||||||
funReplacement: oldSid,
|
MessageIdData(msg.sid),
|
||||||
),
|
]),
|
||||||
);
|
);
|
||||||
_log.finest(
|
_log.finest(
|
||||||
'Sent message with file upload for ${job.path} to $recipient',
|
'Sent message with file upload for ${job.path} to $recipient',
|
||||||
|
@ -1,12 +1,32 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
|
import 'package:moxxyv2/service/conversation.dart';
|
||||||
import 'package:moxxyv2/service/roster.dart';
|
import 'package:moxxyv2/service/roster.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/service/xmpp_state.dart';
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
import 'package:moxxyv2/shared/models/roster.dart';
|
import 'package:moxxyv2/shared/models/roster.dart';
|
||||||
|
|
||||||
|
/// Update the "showAddToRoster" state of the conversation with jid [jid] to
|
||||||
|
/// [showAddToRoster], if the conversation exists.
|
||||||
|
Future<void> updateConversation(String jid, bool showAddToRoster) async {
|
||||||
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
|
final newConversation = await cs.createOrUpdateConversation(
|
||||||
|
jid,
|
||||||
|
update: (conversation) async {
|
||||||
|
final c = conversation.copyWith(
|
||||||
|
showAddToRoster: showAddToRoster,
|
||||||
|
);
|
||||||
|
cs.setConversation(c);
|
||||||
|
return c;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (newConversation != null) {
|
||||||
|
sendEvent(ConversationUpdatedEvent(conversation: newConversation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MoxxyRosterStateManager extends BaseRosterStateManager {
|
class MoxxyRosterStateManager extends BaseRosterStateManager {
|
||||||
@override
|
@override
|
||||||
Future<RosterCacheLoadResult> loadRosterCache() async {
|
Future<RosterCacheLoadResult> loadRosterCache() async {
|
||||||
@ -45,6 +65,7 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
|||||||
// Remove stale items
|
// Remove stale items
|
||||||
for (final jid in removed) {
|
for (final jid in removed) {
|
||||||
await rs.removeRosterItemByJid(jid);
|
await rs.removeRosterItemByJid(jid);
|
||||||
|
await updateConversation(jid, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new roster items
|
// Create new roster items
|
||||||
@ -54,21 +75,23 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
|||||||
// Skip adding items twice
|
// Skip adding items twice
|
||||||
if (exists) continue;
|
if (exists) continue;
|
||||||
|
|
||||||
rosterAdded.add(
|
final newRosterItem = await rs.addRosterItemFromData(
|
||||||
await rs.addRosterItemFromData(
|
'',
|
||||||
'',
|
'',
|
||||||
'',
|
item.jid,
|
||||||
item.jid,
|
item.name ?? item.jid.split('@').first,
|
||||||
item.name ?? item.jid.split('@').first,
|
item.subscription,
|
||||||
item.subscription,
|
item.ask ?? '',
|
||||||
item.ask ?? '',
|
false,
|
||||||
false,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
groups: item.groups,
|
||||||
groups: item.groups,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
rosterAdded.add(newRosterItem);
|
||||||
|
|
||||||
|
// Update the cached conversation item
|
||||||
|
await updateConversation(item.jid, newRosterItem.showAddToRosterButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modified items
|
// Update modified items
|
||||||
@ -80,15 +103,17 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
rosterModified.add(
|
final newRosterItem = await rs.updateRosterItem(
|
||||||
await rs.updateRosterItem(
|
ritem.id,
|
||||||
ritem.id,
|
title: item.name,
|
||||||
title: item.name,
|
subscription: item.subscription,
|
||||||
subscription: item.subscription,
|
ask: item.ask,
|
||||||
ask: item.ask,
|
groups: item.groups,
|
||||||
groups: item.groups,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
rosterModified.add(newRosterItem);
|
||||||
|
|
||||||
|
// Update the cached conversation item
|
||||||
|
await updateConversation(item.jid, newRosterItem.showAddToRosterButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the UI
|
// Tell the UI
|
||||||
|
@ -36,7 +36,7 @@ class NotificationsService {
|
|||||||
MessageNotificationTappedEvent(
|
MessageNotificationTappedEvent(
|
||||||
conversationJid: action.payload!['conversationJid']!,
|
conversationJid: action.payload!['conversationJid']!,
|
||||||
title: action.payload!['title']!,
|
title: action.payload!['title']!,
|
||||||
avatarUrl: action.payload!['avatarUrl']!,
|
avatarPath: action.payload!['avatarPath']!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (action.buttonKeyPressed == _notificationActionKeyRead) {
|
} else if (action.buttonKeyPressed == _notificationActionKeyRead) {
|
||||||
@ -110,8 +110,8 @@ class NotificationsService {
|
|||||||
final title =
|
final title =
|
||||||
contactIntegrationEnabled ? c.contactDisplayName ?? c.title : c.title;
|
contactIntegrationEnabled ? c.contactDisplayName ?? c.title : c.title;
|
||||||
final avatarPath = contactIntegrationEnabled
|
final avatarPath = contactIntegrationEnabled
|
||||||
? c.contactAvatarPath ?? c.avatarUrl
|
? c.contactAvatarPath ?? c.avatarPath
|
||||||
: c.avatarUrl;
|
: c.avatarPath;
|
||||||
|
|
||||||
await AwesomeNotifications().createNotification(
|
await AwesomeNotifications().createNotification(
|
||||||
content: NotificationContent(
|
content: NotificationContent(
|
||||||
@ -131,7 +131,7 @@ class NotificationsService {
|
|||||||
'conversationJid': c.jid,
|
'conversationJid': c.jid,
|
||||||
'sid': m.sid,
|
'sid': m.sid,
|
||||||
'title': title,
|
'title': title,
|
||||||
'avatarUrl': avatarPath,
|
'avatarPath': avatarPath,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actionButtons: [
|
actionButtons: [
|
||||||
|
@ -8,7 +8,6 @@ import 'package:moxxyv2/service/database/database.dart';
|
|||||||
import 'package:moxxyv2/service/database/helpers.dart';
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
import 'package:moxxyv2/service/not_specified.dart';
|
import 'package:moxxyv2/service/not_specified.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/service/subscription.dart';
|
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
import 'package:moxxyv2/shared/models/roster.dart';
|
import 'package:moxxyv2/shared/models/roster.dart';
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ class RosterService {
|
|||||||
|
|
||||||
/// Wrapper around [DatabaseService]'s addRosterItemFromData that updates the cache.
|
/// Wrapper around [DatabaseService]'s addRosterItemFromData that updates the cache.
|
||||||
Future<RosterItem> addRosterItemFromData(
|
Future<RosterItem> addRosterItemFromData(
|
||||||
String avatarUrl,
|
String avatarPath,
|
||||||
String avatarHash,
|
String avatarHash,
|
||||||
String jid,
|
String jid,
|
||||||
String title,
|
String title,
|
||||||
@ -47,7 +46,7 @@ class RosterService {
|
|||||||
// TODO(PapaTutuWawa): Handle groups
|
// TODO(PapaTutuWawa): Handle groups
|
||||||
final i = RosterItem(
|
final i = RosterItem(
|
||||||
-1,
|
-1,
|
||||||
avatarUrl,
|
avatarPath,
|
||||||
avatarHash,
|
avatarHash,
|
||||||
jid,
|
jid,
|
||||||
title,
|
title,
|
||||||
@ -76,7 +75,7 @@ class RosterService {
|
|||||||
/// Wrapper around [DatabaseService]'s updateRosterItem that updates the cache.
|
/// Wrapper around [DatabaseService]'s updateRosterItem that updates the cache.
|
||||||
Future<RosterItem> updateRosterItem(
|
Future<RosterItem> updateRosterItem(
|
||||||
int id, {
|
int id, {
|
||||||
String? avatarUrl,
|
String? avatarPath,
|
||||||
String? avatarHash,
|
String? avatarHash,
|
||||||
String? title,
|
String? title,
|
||||||
String? subscription,
|
String? subscription,
|
||||||
@ -89,8 +88,8 @@ class RosterService {
|
|||||||
}) async {
|
}) async {
|
||||||
final i = <String, dynamic>{};
|
final i = <String, dynamic>{};
|
||||||
|
|
||||||
if (avatarUrl != null) {
|
if (avatarPath != null) {
|
||||||
i['avatarUrl'] = avatarUrl;
|
i['avatarPath'] = avatarPath;
|
||||||
}
|
}
|
||||||
if (avatarHash != null) {
|
if (avatarHash != null) {
|
||||||
i['avatarHash'] = avatarHash;
|
i['avatarHash'] = avatarHash;
|
||||||
@ -197,7 +196,7 @@ class RosterService {
|
|||||||
/// and, if it was successful, create the database entry. Returns the
|
/// and, if it was successful, create the database entry. Returns the
|
||||||
/// [RosterItem] model object.
|
/// [RosterItem] model object.
|
||||||
Future<RosterItem> addToRosterWrapper(
|
Future<RosterItem> addToRosterWrapper(
|
||||||
String avatarUrl,
|
String avatarPath,
|
||||||
String avatarHash,
|
String avatarHash,
|
||||||
String jid,
|
String jid,
|
||||||
String title,
|
String title,
|
||||||
@ -205,7 +204,7 @@ class RosterService {
|
|||||||
final css = GetIt.I.get<ContactsService>();
|
final css = GetIt.I.get<ContactsService>();
|
||||||
final contactId = await css.getContactIdForJid(jid);
|
final contactId = await css.getContactIdForJid(jid);
|
||||||
final item = await addRosterItemFromData(
|
final item = await addRosterItemFromData(
|
||||||
avatarUrl,
|
avatarPath,
|
||||||
avatarHash,
|
avatarHash,
|
||||||
jid,
|
jid,
|
||||||
title,
|
title,
|
||||||
@ -217,14 +216,19 @@ class RosterService {
|
|||||||
await css.getContactDisplayName(contactId),
|
await css.getContactDisplayName(contactId),
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await GetIt.I
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
.get<XmppConnection>()
|
final result = await conn.getRosterManager()!.addToRoster(jid, title);
|
||||||
.getRosterManager()!
|
|
||||||
.addToRoster(jid, title);
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// TODO(Unknown): Signal error?
|
// TODO(Unknown): Signal error?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final to = JID.fromString(jid);
|
||||||
|
final preApproval =
|
||||||
|
await conn.getPresenceManager()!.preApproveSubscription(to);
|
||||||
|
if (!preApproval) {
|
||||||
|
await conn.getPresenceManager()!.requestSubscription(to);
|
||||||
|
}
|
||||||
|
|
||||||
sendEvent(RosterDiffEvent(added: [item]));
|
sendEvent(RosterDiffEvent(added: [item]));
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@ -236,14 +240,14 @@ class RosterService {
|
|||||||
String jid, {
|
String jid, {
|
||||||
bool unsubscribe = true,
|
bool unsubscribe = true,
|
||||||
}) async {
|
}) async {
|
||||||
final roster = GetIt.I.get<XmppConnection>().getRosterManager()!;
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
final roster = conn.getRosterManager()!;
|
||||||
|
final pm = conn.getManagerById<PresenceManager>(presenceManager)!;
|
||||||
final result = await roster.removeFromRoster(jid);
|
final result = await roster.removeFromRoster(jid);
|
||||||
if (result == RosterRemovalResult.okay ||
|
if (result == RosterRemovalResult.okay ||
|
||||||
result == RosterRemovalResult.itemNotFound) {
|
result == RosterRemovalResult.itemNotFound) {
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
GetIt.I
|
await pm.unsubscribe(JID.fromString(jid));
|
||||||
.get<SubscriptionRequestService>()
|
|
||||||
.sendUnsubscriptionRequest(jid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.finest('Removing from roster maybe worked. Removing from database');
|
_log.finest('Removing from roster maybe worked. Removing from database');
|
||||||
|
@ -33,7 +33,6 @@ import 'package:moxxyv2/service/preferences.dart';
|
|||||||
import 'package:moxxyv2/service/reactions.dart';
|
import 'package:moxxyv2/service/reactions.dart';
|
||||||
import 'package:moxxyv2/service/roster.dart';
|
import 'package:moxxyv2/service/roster.dart';
|
||||||
import 'package:moxxyv2/service/stickers.dart';
|
import 'package:moxxyv2/service/stickers.dart';
|
||||||
import 'package:moxxyv2/service/subscription.dart';
|
|
||||||
import 'package:moxxyv2/service/xmpp.dart';
|
import 'package:moxxyv2/service/xmpp.dart';
|
||||||
import 'package:moxxyv2/service/xmpp_state.dart';
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
@ -175,9 +174,6 @@ Future<void> entrypoint() async {
|
|||||||
GetIt.I.registerSingleton<ContactsService>(ContactsService());
|
GetIt.I.registerSingleton<ContactsService>(ContactsService());
|
||||||
GetIt.I.registerSingleton<StickersService>(StickersService());
|
GetIt.I.registerSingleton<StickersService>(StickersService());
|
||||||
GetIt.I.registerSingleton<XmppStateService>(XmppStateService());
|
GetIt.I.registerSingleton<XmppStateService>(XmppStateService());
|
||||||
GetIt.I.registerSingleton<SubscriptionRequestService>(
|
|
||||||
SubscriptionRequestService(),
|
|
||||||
);
|
|
||||||
GetIt.I.registerSingleton<FilesService>(FilesService());
|
GetIt.I.registerSingleton<FilesService>(FilesService());
|
||||||
GetIt.I.registerSingleton<ReactionsService>(ReactionsService());
|
GetIt.I.registerSingleton<ReactionsService>(ReactionsService());
|
||||||
final xmpp = XmppService();
|
final xmpp = XmppService();
|
||||||
@ -211,6 +207,7 @@ Future<void> entrypoint() async {
|
|||||||
StreamManagementNegotiator(),
|
StreamManagementNegotiator(),
|
||||||
CSINegotiator(),
|
CSINegotiator(),
|
||||||
RosterFeatureNegotiator(),
|
RosterFeatureNegotiator(),
|
||||||
|
PresenceNegotiator(),
|
||||||
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
||||||
SaslScramNegotiator(9, '', '', ScramHashType.sha256),
|
SaslScramNegotiator(9, '', '', ScramHashType.sha256),
|
||||||
SaslScramNegotiator(8, '', '', ScramHashType.sha1),
|
SaslScramNegotiator(8, '', '', ScramHashType.sha1),
|
||||||
@ -230,7 +227,6 @@ Future<void> entrypoint() async {
|
|||||||
CSIManager(),
|
CSIManager(),
|
||||||
CarbonsManager(),
|
CarbonsManager(),
|
||||||
PubSubManager(),
|
PubSubManager(),
|
||||||
VCardManager(),
|
|
||||||
UserAvatarManager(),
|
UserAvatarManager(),
|
||||||
StableIdManager(),
|
StableIdManager(),
|
||||||
MessageDeliveryReceiptManager(),
|
MessageDeliveryReceiptManager(),
|
||||||
@ -249,6 +245,7 @@ Future<void> entrypoint() async {
|
|||||||
LastMessageCorrectionManager(),
|
LastMessageCorrectionManager(),
|
||||||
MessageReactionsManager(),
|
MessageReactionsManager(),
|
||||||
StickersManager(),
|
StickersManager(),
|
||||||
|
MessageProcessingHintManager(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
GetIt.I.registerSingleton<XmppConnection>(connection);
|
GetIt.I.registerSingleton<XmppConnection>(connection);
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
|
||||||
import 'package:moxxyv2/service/database/constants.dart';
|
|
||||||
import 'package:moxxyv2/service/database/database.dart';
|
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
|
||||||
import 'package:synchronized/synchronized.dart';
|
|
||||||
|
|
||||||
class SubscriptionRequestService {
|
|
||||||
List<String>? _subscriptionRequests;
|
|
||||||
|
|
||||||
final Lock _lock = Lock();
|
|
||||||
|
|
||||||
/// Only load data from the database into
|
|
||||||
/// [SubscriptionRequestService._subscriptionRequests] when the cache has not yet
|
|
||||||
/// been loaded.
|
|
||||||
Future<void> _loadSubscriptionRequestsIfNeeded() async {
|
|
||||||
await _lock.synchronized(() async {
|
|
||||||
_subscriptionRequests ??= List<String>.from(
|
|
||||||
(await GetIt.I
|
|
||||||
.get<DatabaseService>()
|
|
||||||
.database
|
|
||||||
.query(subscriptionsTable))
|
|
||||||
.map((m) => m['jid']! as String)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> getSubscriptionRequests() async {
|
|
||||||
await _loadSubscriptionRequestsIfNeeded();
|
|
||||||
return _subscriptionRequests!;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addSubscriptionRequest(String jid) async {
|
|
||||||
await _loadSubscriptionRequestsIfNeeded();
|
|
||||||
|
|
||||||
await _lock.synchronized(() async {
|
|
||||||
if (!_subscriptionRequests!.contains(jid)) {
|
|
||||||
_subscriptionRequests!.add(jid);
|
|
||||||
|
|
||||||
await GetIt.I.get<DatabaseService>().database.insert(
|
|
||||||
subscriptionsTable,
|
|
||||||
{
|
|
||||||
'jid': jid,
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeSubscriptionRequest(String jid) async {
|
|
||||||
await _loadSubscriptionRequestsIfNeeded();
|
|
||||||
|
|
||||||
await _lock.synchronized(() async {
|
|
||||||
if (_subscriptionRequests!.contains(jid)) {
|
|
||||||
_subscriptionRequests!.remove(jid);
|
|
||||||
await GetIt.I.get<DatabaseService>().database.delete(
|
|
||||||
subscriptionsTable,
|
|
||||||
where: 'jid = ?',
|
|
||||||
whereArgs: [jid],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> hasPendingSubscriptionRequest(String jid) async {
|
|
||||||
return (await getSubscriptionRequests()).contains(jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
PresenceManager get _presence =>
|
|
||||||
GetIt.I.get<XmppConnection>().getPresenceManager()!;
|
|
||||||
|
|
||||||
/// Accept a subscription request from [jid].
|
|
||||||
Future<void> acceptSubscriptionRequest(String jid) async {
|
|
||||||
_presence.sendSubscriptionRequestApproval(jid);
|
|
||||||
await removeSubscriptionRequest(jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reject a subscription request from [jid].
|
|
||||||
Future<void> rejectSubscriptionRequest(String jid) async {
|
|
||||||
_presence.sendSubscriptionRequestRejection(jid);
|
|
||||||
await removeSubscriptionRequest(jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a subscription request to [jid].
|
|
||||||
void sendSubscriptionRequest(String jid) {
|
|
||||||
_presence.sendSubscriptionRequest(jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a presence subscription with [jid].
|
|
||||||
void sendUnsubscriptionRequest(String jid) {
|
|
||||||
_presence.sendUnsubscriptionRequest(jid);
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,7 +29,6 @@ import 'package:moxxyv2/service/preferences.dart';
|
|||||||
import 'package:moxxyv2/service/reactions.dart';
|
import 'package:moxxyv2/service/reactions.dart';
|
||||||
import 'package:moxxyv2/service/roster.dart';
|
import 'package:moxxyv2/service/roster.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/service/subscription.dart';
|
|
||||||
import 'package:moxxyv2/service/xmpp_state.dart';
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
import 'package:moxxyv2/shared/error_types.dart';
|
import 'package:moxxyv2/shared/error_types.dart';
|
||||||
import 'package:moxxyv2/shared/eventhandler.dart';
|
import 'package:moxxyv2/shared/eventhandler.dart';
|
||||||
@ -56,7 +55,7 @@ class XmppService {
|
|||||||
_onDeliveryReceiptReceived,
|
_onDeliveryReceiptReceived,
|
||||||
),
|
),
|
||||||
EventTypeMatcher<ChatMarkerEvent>(_onChatMarker),
|
EventTypeMatcher<ChatMarkerEvent>(_onChatMarker),
|
||||||
EventTypeMatcher<AvatarUpdatedEvent>(_onAvatarUpdated),
|
EventTypeMatcher<UserAvatarUpdatedEvent>(_onAvatarUpdated),
|
||||||
EventTypeMatcher<StanzaAckedEvent>(_onStanzaAcked),
|
EventTypeMatcher<StanzaAckedEvent>(_onStanzaAcked),
|
||||||
EventTypeMatcher<MessageEvent>(_onMessage),
|
EventTypeMatcher<MessageEvent>(_onMessage),
|
||||||
EventTypeMatcher<BlocklistBlockPushEvent>(_onBlocklistBlockPush),
|
EventTypeMatcher<BlocklistBlockPushEvent>(_onBlocklistBlockPush),
|
||||||
@ -184,14 +183,15 @@ class XmppService {
|
|||||||
|
|
||||||
if (conversation?.type != ConversationType.note) {
|
if (conversation?.type != ConversationType.note) {
|
||||||
// Send the correction
|
// Send the correction
|
||||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
final manager = conn.getManagerById<MessageManager>(messageManager)!;
|
||||||
MessageDetails(
|
await manager.sendMessage(
|
||||||
to: recipient,
|
JID.fromString(recipient),
|
||||||
body: newBody,
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
lastMessageCorrectionId: oldId,
|
MessageBodyData(newBody),
|
||||||
chatState: chatState,
|
LastMessageCorrectionData(oldId),
|
||||||
),
|
if (chatState != null) chatState,
|
||||||
);
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,28 +259,39 @@ class XmppService {
|
|||||||
|
|
||||||
if (conversation?.type == ConversationType.chat) {
|
if (conversation?.type == ConversationType.chat) {
|
||||||
final moxxmppSticker = sticker?.toMoxxmpp();
|
final moxxmppSticker = sticker?.toMoxxmpp();
|
||||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
final manager = conn.getManagerById<MessageManager>(messageManager)!;
|
||||||
MessageDetails(
|
|
||||||
to: recipient,
|
await manager.sendMessage(
|
||||||
body: body,
|
JID.fromString(recipient),
|
||||||
requestDeliveryReceipt: true,
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
id: sid,
|
MessageBodyData(body),
|
||||||
originId: originId,
|
const MarkableData(true),
|
||||||
quoteBody: createFallbackBodyForQuotedMessage(quotedMessage),
|
MessageIdData(sid),
|
||||||
quoteFrom: quotedMessage?.sender,
|
StableIdData(originId, null),
|
||||||
quoteId: quotedMessage?.sid,
|
|
||||||
chatState: chatState,
|
if (sticker != null && moxxmppSticker != null)
|
||||||
shouldEncrypt: conversation!.encrypted,
|
StickersData(
|
||||||
stickerPackId: sticker?.stickerPackId,
|
sticker.stickerPackId,
|
||||||
sfs: moxxmppSticker != null
|
StatelessFileSharingData(
|
||||||
? StatelessFileSharingData(
|
moxxmppSticker.metadata,
|
||||||
moxxmppSticker.metadata,
|
moxxmppSticker.sources,
|
||||||
moxxmppSticker.sources,
|
),
|
||||||
)
|
|
||||||
: null,
|
|
||||||
setOOBFallbackBody: sticker != null ? false : true,
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
|
// Optional chat state
|
||||||
|
if (chatState != null) chatState,
|
||||||
|
|
||||||
|
// Prepare the appropriate quote
|
||||||
|
if (quotedMessage != null)
|
||||||
|
ReplyData.fromQuoteData(
|
||||||
|
quotedMessage.sid,
|
||||||
|
QuoteData.fromBodies(
|
||||||
|
createFallbackBodyForQuotedMessage(quotedMessage),
|
||||||
|
body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendEvent(
|
sendEvent(
|
||||||
@ -290,7 +301,9 @@ class XmppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MediaFileLocation? _getEmbeddedFile(MessageEvent event) {
|
MediaFileLocation? _getEmbeddedFile(MessageEvent event) {
|
||||||
if (event.sfs?.sources.isNotEmpty ?? false) {
|
final sfs = event.extensions.get<StatelessFileSharingData>();
|
||||||
|
final oob = event.extensions.get<OOBData>();
|
||||||
|
if (sfs?.sources.isNotEmpty ?? false) {
|
||||||
// final source = firstWhereOrNull(
|
// final source = firstWhereOrNull(
|
||||||
// event.sfs!.sources,
|
// event.sfs!.sources,
|
||||||
// (StatelessFileSharingSource source) {
|
// (StatelessFileSharingSource source) {
|
||||||
@ -300,14 +313,14 @@ class XmppService {
|
|||||||
// );
|
// );
|
||||||
|
|
||||||
final hasUrlSource = firstWhereOrNull(
|
final hasUrlSource = firstWhereOrNull(
|
||||||
event.sfs!.sources,
|
sfs!.sources,
|
||||||
(src) => src is StatelessFileSharingUrlSource,
|
(src) => src is StatelessFileSharingUrlSource,
|
||||||
) !=
|
) !=
|
||||||
null;
|
null;
|
||||||
|
|
||||||
final name = event.sfs!.metadata.name;
|
final name = sfs.metadata.name;
|
||||||
if (hasUrlSource) {
|
if (hasUrlSource) {
|
||||||
final sources = event.sfs!.sources
|
final sources = sfs.sources
|
||||||
.whereType<StatelessFileSharingUrlSource>()
|
.whereType<StatelessFileSharingUrlSource>()
|
||||||
.map((src) => src.url)
|
.map((src) => src.url)
|
||||||
.toList();
|
.toList();
|
||||||
@ -317,13 +330,13 @@ class XmppService {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
event.sfs!.metadata.hashes,
|
sfs.metadata.hashes,
|
||||||
null,
|
null,
|
||||||
event.sfs!.metadata.size,
|
sfs.metadata.size,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final encryptedSource = firstWhereOrNull(
|
final encryptedSource = firstWhereOrNull(
|
||||||
event.sfs!.sources,
|
sfs.sources,
|
||||||
(src) => src is StatelessFileSharingEncryptedSource,
|
(src) => src is StatelessFileSharingEncryptedSource,
|
||||||
)! as StatelessFileSharingEncryptedSource;
|
)! as StatelessFileSharingEncryptedSource;
|
||||||
|
|
||||||
@ -335,15 +348,15 @@ class XmppService {
|
|||||||
encryptedSource.encryption.toNamespace(),
|
encryptedSource.encryption.toNamespace(),
|
||||||
encryptedSource.key,
|
encryptedSource.key,
|
||||||
encryptedSource.iv,
|
encryptedSource.iv,
|
||||||
event.sfs?.metadata.hashes,
|
sfs.metadata.hashes,
|
||||||
encryptedSource.hashes,
|
encryptedSource.hashes,
|
||||||
event.sfs!.metadata.size,
|
sfs.metadata.size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (event.oob != null) {
|
} else if (oob != null) {
|
||||||
return MediaFileLocation(
|
return MediaFileLocation(
|
||||||
[event.oob!.url!],
|
[oob.url!],
|
||||||
filenameFromUrl(event.oob!.url!),
|
filenameFromUrl(oob.url!),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -360,45 +373,36 @@ class XmppService {
|
|||||||
final result = await GetIt.I
|
final result = await GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getDiscoManager()!
|
.getDiscoManager()!
|
||||||
.discoInfoQuery(event.fromJid);
|
.discoInfoQuery(event.from);
|
||||||
if (result.isType<DiscoError>()) return;
|
if (result.isType<DiscoError>()) return;
|
||||||
|
|
||||||
final info = result.get<DiscoInfo>();
|
final info = result.get<DiscoInfo>();
|
||||||
if (event.isMarkable && info.features.contains(chatMarkersXmlns)) {
|
final isMarkable =
|
||||||
unawaited(
|
event.extensions.get<MarkableData>()?.isMarkable ?? false;
|
||||||
GetIt.I.get<XmppConnection>().sendStanza(
|
final deliveryReceiptRequested =
|
||||||
StanzaDetails(
|
event.extensions.get<MessageDeliveryReceiptData>()?.receiptRequested ??
|
||||||
Stanza.message(
|
false;
|
||||||
to: event.fromJid.toBare().toString(),
|
final originId = event.extensions.get<StableIdData>()?.originId;
|
||||||
type: event.type,
|
final manager = GetIt.I
|
||||||
children: [
|
.get<XmppConnection>()
|
||||||
makeChatMarker(
|
.getManagerById<MessageManager>(messageManager)!;
|
||||||
'received',
|
if (isMarkable && info.features.contains(chatMarkersXmlns)) {
|
||||||
event.originId ?? event.sid,
|
await manager.sendMessage(
|
||||||
)
|
event.from.toBare(),
|
||||||
],
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
),
|
ChatMarkerData(
|
||||||
awaitable: false,
|
ChatMarker.received,
|
||||||
),
|
originId ?? event.id,
|
||||||
),
|
)
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
} else if (event.deliveryReceiptRequested &&
|
} else if (deliveryReceiptRequested &&
|
||||||
info.features.contains(deliveryXmlns)) {
|
info.features.contains(deliveryXmlns)) {
|
||||||
unawaited(
|
await manager.sendMessage(
|
||||||
GetIt.I.get<XmppConnection>().sendStanza(
|
event.from.toBare(),
|
||||||
StanzaDetails(
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
Stanza.message(
|
MessageDeliveryReceivedData(originId ?? event.id),
|
||||||
to: event.fromJid.toBare().toString(),
|
]),
|
||||||
type: event.type,
|
|
||||||
children: [
|
|
||||||
makeMessageDeliveryResponse(
|
|
||||||
event.originId ?? event.sid,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
awaitable: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -410,19 +414,14 @@ class XmppService {
|
|||||||
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
if (!prefs.sendChatMarkers) return;
|
if (!prefs.sendChatMarkers) return;
|
||||||
|
|
||||||
unawaited(
|
final manager = GetIt.I
|
||||||
GetIt.I.get<XmppConnection>().sendStanza(
|
.get<XmppConnection>()
|
||||||
StanzaDetails(
|
.getManagerById<MessageManager>(messageManager)!;
|
||||||
Stanza.message(
|
await manager.sendMessage(
|
||||||
to: to,
|
JID.fromString(to),
|
||||||
type: 'chat',
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
children: [
|
ChatMarkerData(ChatMarker.displayed, sid),
|
||||||
makeChatMarker('displayed', sid),
|
]),
|
||||||
],
|
|
||||||
),
|
|
||||||
awaitable: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,7 +600,7 @@ class XmppService {
|
|||||||
rosterItem?.title ?? recipient.split('@').first,
|
rosterItem?.title ?? recipient.split('@').first,
|
||||||
lastMessages[recipient],
|
lastMessages[recipient],
|
||||||
ConversationType.chat,
|
ConversationType.chat,
|
||||||
rosterItem?.avatarUrl ?? '',
|
rosterItem?.avatarPath ?? '',
|
||||||
recipient,
|
recipient,
|
||||||
0,
|
0,
|
||||||
DateTime.now().millisecondsSinceEpoch,
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
@ -643,6 +642,7 @@ class XmppService {
|
|||||||
|
|
||||||
// Requesting Upload slots and uploading
|
// Requesting Upload slots and uploading
|
||||||
final hfts = GetIt.I.get<HttpFileTransferService>();
|
final hfts = GetIt.I.get<HttpFileTransferService>();
|
||||||
|
final manager = conn.getManagerById<MessageManager>(messageManager)!;
|
||||||
for (final path in paths) {
|
for (final path in paths) {
|
||||||
final pathMime = lookupMimeType(path);
|
final pathMime = lookupMimeType(path);
|
||||||
|
|
||||||
@ -661,20 +661,21 @@ class XmppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (recipient != '') {
|
if (recipient != '') {
|
||||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
await manager.sendMessage(
|
||||||
MessageDetails(
|
JID.fromString(recipient),
|
||||||
to: recipient,
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
id: messages[path]![recipient]!.sid,
|
MessageIdData(messages[path]![recipient]!.sid),
|
||||||
fun: FileMetadataData(
|
FileUploadNotificationData(
|
||||||
// TODO(Unknown): Maybe add media type specific metadata
|
FileMetadataData(
|
||||||
mediaType: lookupMimeType(path),
|
// TODO(Unknown): Maybe add media type specific metadata
|
||||||
name: pathlib.basename(path),
|
mediaType: lookupMimeType(path),
|
||||||
size: File(path).statSync().size,
|
name: pathlib.basename(path),
|
||||||
thumbnails: thumbnails[path] ?? [],
|
size: File(path).statSync().size,
|
||||||
),
|
thumbnails: thumbnails[path] ?? [],
|
||||||
shouldEncrypt: encrypt[recipient]!,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -761,18 +762,24 @@ class XmppService {
|
|||||||
unawaited(_initializeOmemoService(settings.jid.toString()));
|
unawaited(_initializeOmemoService(settings.jid.toString()));
|
||||||
|
|
||||||
if (!event.resumed) {
|
if (!event.resumed) {
|
||||||
|
// Reset the avatar service's cache
|
||||||
|
GetIt.I.get<AvatarService>().resetCache();
|
||||||
|
|
||||||
// Reset the blocking service's cache
|
// Reset the blocking service's cache
|
||||||
GetIt.I.get<BlocklistService>().onNewConnection();
|
GetIt.I.get<BlocklistService>().onNewConnection();
|
||||||
|
|
||||||
// Reset the OMEMO cache
|
// Reset the OMEMO cache
|
||||||
GetIt.I.get<OmemoService>().onNewConnection();
|
GetIt.I.get<OmemoService>().onNewConnection();
|
||||||
|
|
||||||
// Enable carbons
|
// Enable carbons, if they're not already enabled (e.g. by using SASL2)
|
||||||
final carbonsResult = await connection
|
final cm = connection.getManagerById<CarbonsManager>(carbonsManager)!;
|
||||||
.getManagerById<CarbonsManager>(carbonsManager)!
|
if (!cm.isEnabled) {
|
||||||
.enableCarbons();
|
final carbonsResult = await cm.enableCarbons();
|
||||||
if (!carbonsResult) {
|
if (!carbonsResult) {
|
||||||
_log.warning('Failed to enable carbons');
|
_log.warning('Failed to enable carbons');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_log.info('Not enabling carbons as they are already enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// In section 5 of XEP-0198 it says that a client should not request the roster
|
// In section 5 of XEP-0198 it says that a client should not request the roster
|
||||||
@ -781,22 +788,9 @@ class XmppService {
|
|||||||
.getManagerById<RosterManager>(rosterManager)!
|
.getManagerById<RosterManager>(rosterManager)!
|
||||||
.requestRoster();
|
.requestRoster();
|
||||||
|
|
||||||
// TODO(Unknown): Once groupchats come into the equation, this gets trickier
|
|
||||||
final roster = await GetIt.I.get<RosterService>().getRoster();
|
|
||||||
for (final item in roster) {
|
|
||||||
await GetIt.I
|
|
||||||
.get<AvatarService>()
|
|
||||||
.fetchAndUpdateAvatarForJid(item.jid, item.avatarHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
await GetIt.I.get<BlocklistService>().getBlocklist();
|
await GetIt.I.get<BlocklistService>().getBlocklist();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we display our own avatar correctly.
|
|
||||||
// Note that this only requests the avatar if its hash differs from the locally cached avatar's.
|
|
||||||
// TODO(Unknown): Maybe don't do this on mobile Internet
|
|
||||||
unawaited(GetIt.I.get<AvatarService>().requestOwnAvatar());
|
|
||||||
|
|
||||||
if (_loginTriggeredFromUI) {
|
if (_loginTriggeredFromUI) {
|
||||||
// TODO(Unknown): Trigger another event so the UI can see this aswell
|
// TODO(Unknown): Trigger another event so the UI can see this aswell
|
||||||
await GetIt.I.get<XmppStateService>().modifyXmppState(
|
await GetIt.I.get<XmppStateService>().modifyXmppState(
|
||||||
@ -808,6 +802,10 @@ class XmppService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendEvent(
|
||||||
|
StreamNegotiationsCompletedEvent(resumed: event.resumed),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onConnectionStateChanged(
|
Future<void> _onConnectionStateChanged(
|
||||||
@ -837,20 +835,24 @@ class XmppService {
|
|||||||
SubscriptionRequestReceivedEvent event, {
|
SubscriptionRequestReceivedEvent event, {
|
||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
final jid = event.from.toBare().toString();
|
final jid = event.from.toBare();
|
||||||
|
|
||||||
// Auto-accept if the JID is in the roster
|
// Auto-accept if the JID is in the roster
|
||||||
final rs = GetIt.I.get<RosterService>();
|
final rs = GetIt.I.get<RosterService>();
|
||||||
final srs = GetIt.I.get<SubscriptionRequestService>();
|
final rosterItem = await rs.getRosterItemByJid(jid.toString());
|
||||||
final rosterItem = await rs.getRosterItemByJid(jid);
|
|
||||||
if (rosterItem != null) {
|
if (rosterItem != null) {
|
||||||
await srs.acceptSubscriptionRequest(jid);
|
final pm = GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<PresenceManager>(presenceManager)!;
|
||||||
|
|
||||||
|
switch (rosterItem.subscription) {
|
||||||
|
case 'from':
|
||||||
|
await pm.acceptSubscriptionRequest(jid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await GetIt.I.get<SubscriptionRequestService>().addSubscriptionRequest(
|
|
||||||
event.from.toBare().toString(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDeliveryReceiptReceived(
|
Future<void> _onDeliveryReceiptReceived(
|
||||||
@ -900,12 +902,12 @@ class XmppService {
|
|||||||
final msg = await ms.updateMessage(
|
final msg = await ms.updateMessage(
|
||||||
dbMsg.id,
|
dbMsg.id,
|
||||||
received: dbMsg.received ||
|
received: dbMsg.received ||
|
||||||
event.type == 'received' ||
|
event.type == ChatMarker.received ||
|
||||||
event.type == 'displayed' ||
|
event.type == ChatMarker.displayed ||
|
||||||
event.type == 'acknowledged',
|
event.type == ChatMarker.acknowledged,
|
||||||
displayed: dbMsg.displayed ||
|
displayed: dbMsg.displayed ||
|
||||||
event.type == 'displayed' ||
|
event.type == ChatMarker.displayed ||
|
||||||
event.type == 'acknowledged',
|
event.type == ChatMarker.acknowledged,
|
||||||
);
|
);
|
||||||
sendEvent(MessageUpdatedEvent(message: msg));
|
sendEvent(MessageUpdatedEvent(message: msg));
|
||||||
|
|
||||||
@ -935,14 +937,21 @@ class XmppService {
|
|||||||
|
|
||||||
/// Return true if [event] describes a message that we want to display.
|
/// Return true if [event] describes a message that we want to display.
|
||||||
bool _isMessageEventMessage(MessageEvent event) {
|
bool _isMessageEventMessage(MessageEvent event) {
|
||||||
return event.body.isNotEmpty || event.sfs != null || event.fun != null;
|
final body = event.extensions.get<MessageBodyData>()?.body;
|
||||||
|
final sfs = event.extensions.get<StatelessFileSharingData>();
|
||||||
|
final fun = event.extensions.get<FileUploadNotificationData>();
|
||||||
|
|
||||||
|
return (body?.isNotEmpty ?? false) || sfs != null || fun != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the thumbnail data from a message, if existent.
|
/// Extract the thumbnail data from a message, if existent.
|
||||||
String? _getThumbnailData(MessageEvent event) {
|
String? _getThumbnailData(MessageEvent event) {
|
||||||
|
final sfs = event.extensions.get<StatelessFileSharingData>();
|
||||||
|
final fun = event.extensions.get<FileUploadNotificationData>();
|
||||||
|
|
||||||
final thumbnails = firstNotNull([
|
final thumbnails = firstNotNull([
|
||||||
event.sfs?.metadata.thumbnails,
|
sfs?.metadata.thumbnails,
|
||||||
event.fun?.thumbnails,
|
fun?.metadata.thumbnails,
|
||||||
]) ??
|
]) ??
|
||||||
[];
|
[];
|
||||||
for (final i in thumbnails) {
|
for (final i in thumbnails) {
|
||||||
@ -956,27 +965,33 @@ class XmppService {
|
|||||||
|
|
||||||
/// Extract the mime guess from a message, if existent.
|
/// Extract the mime guess from a message, if existent.
|
||||||
String? _getMimeGuess(MessageEvent event) {
|
String? _getMimeGuess(MessageEvent event) {
|
||||||
|
final sfs = event.extensions.get<StatelessFileSharingData>();
|
||||||
|
final fun = event.extensions.get<FileUploadNotificationData>();
|
||||||
|
|
||||||
return firstNotNull([
|
return firstNotNull([
|
||||||
event.sfs?.metadata.mediaType,
|
sfs?.metadata.mediaType,
|
||||||
event.fun?.mediaType,
|
fun?.metadata.mediaType,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the embedded dimensions, if existent.
|
/// Extract the embedded dimensions, if existent.
|
||||||
Size? _getDimensions(MessageEvent event) {
|
Size? _getDimensions(MessageEvent event) {
|
||||||
if (event.sfs != null &&
|
final sfs = event.extensions.get<StatelessFileSharingData>();
|
||||||
event.sfs?.metadata.width != null &&
|
final fun = event.extensions.get<FileUploadNotificationData>();
|
||||||
event.sfs?.metadata.height != null) {
|
|
||||||
|
if (sfs != null &&
|
||||||
|
sfs.metadata.width != null &&
|
||||||
|
sfs.metadata.height != null) {
|
||||||
return Size(
|
return Size(
|
||||||
event.sfs!.metadata.width!.toDouble(),
|
sfs.metadata.width!.toDouble(),
|
||||||
event.sfs!.metadata.height!.toDouble(),
|
sfs.metadata.height!.toDouble(),
|
||||||
);
|
);
|
||||||
} else if (event.fun != null &&
|
} else if (fun != null &&
|
||||||
event.fun?.width != null &&
|
fun.metadata.width != null &&
|
||||||
event.fun?.height != null) {
|
fun.metadata.height != null) {
|
||||||
return Size(
|
return Size(
|
||||||
event.fun!.width!.toDouble(),
|
fun.metadata.width!.toDouble(),
|
||||||
event.fun!.height!.toDouble(),
|
fun.metadata.height!.toDouble(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -987,11 +1002,14 @@ class XmppService {
|
|||||||
/// [embeddedFile] is the possible source of the file. If no file is present, then
|
/// [embeddedFile] is the possible source of the file. If no file is present, then
|
||||||
/// [embeddedFile] is null.
|
/// [embeddedFile] is null.
|
||||||
bool _isFileEmbedded(MessageEvent event, MediaFileLocation? embeddedFile) {
|
bool _isFileEmbedded(MessageEvent event, MediaFileLocation? embeddedFile) {
|
||||||
|
final body = event.extensions.get<MessageBodyData>()?.body;
|
||||||
|
final oob = event.extensions.get<OOBData>();
|
||||||
|
|
||||||
// True if we determine a file to be embedded. Checks if the Url is using HTTPS and
|
// True if we determine a file to be embedded. Checks if the Url is using HTTPS and
|
||||||
// that the message body and the OOB url are the same if the OOB url is not null.
|
// that the message body and the OOB url are the same if the OOB url is not null.
|
||||||
return embeddedFile != null &&
|
return embeddedFile != null &&
|
||||||
Uri.parse(embeddedFile.urls.first).scheme == 'https' &&
|
Uri.parse(embeddedFile.urls.first).scheme == 'https' &&
|
||||||
implies(event.oob != null, event.body == event.oob?.url);
|
implies(oob != null, body == oob?.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a message retraction given the MessageEvent [event].
|
/// Handle a message retraction given the MessageEvent [event].
|
||||||
@ -1001,8 +1019,8 @@ class XmppService {
|
|||||||
) async {
|
) async {
|
||||||
await GetIt.I.get<MessageService>().retractMessage(
|
await GetIt.I.get<MessageService>().retractMessage(
|
||||||
conversationJid,
|
conversationJid,
|
||||||
event.messageRetraction!.id,
|
event.extensions.get<MessageRetractionData>()!.id,
|
||||||
event.fromJid.toBare().toString(),
|
event.from.toBare().toString(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1020,19 +1038,19 @@ class XmppService {
|
|||||||
Future<void> _handleErrorMessage(MessageEvent event) async {
|
Future<void> _handleErrorMessage(MessageEvent event) async {
|
||||||
if (event.error == null) {
|
if (event.error == null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
'Received error for message ${event.sid} without an error element',
|
'Received error for message ${event.id} without an error element',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
final msg = await ms.getMessageByStanzaId(
|
final msg = await ms.getMessageByStanzaId(
|
||||||
event.fromJid.toBare().toString(),
|
event.from.toBare().toString(),
|
||||||
event.sid,
|
event.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (msg == null) {
|
if (msg == null) {
|
||||||
_log.warning('Received error for message ${event.sid} we cannot find');
|
_log.warning('Received error for message ${event.id} we cannot find');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1068,23 +1086,23 @@ class XmppService {
|
|||||||
) async {
|
) async {
|
||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
|
|
||||||
|
final correctionId = event.extensions.get<LastMessageCorrectionData>()!.id;
|
||||||
final msg = await ms.getMessageByStanzaId(
|
final msg = await ms.getMessageByStanzaId(
|
||||||
conversationJid,
|
conversationJid,
|
||||||
event.messageCorrectionId!,
|
correctionId,
|
||||||
);
|
);
|
||||||
if (msg == null) {
|
if (msg == null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
'Received message correction for message ${event.messageCorrectionId} we cannot find.',
|
'Received message correction for message $correctionId we cannot find.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the Jid is allowed to do correct the message
|
// Check if the Jid is allowed to do correct the message
|
||||||
// TODO(Unknown): Maybe use the JID parser?
|
if (msg.senderJid.toBare() != event.from.toBare()) {
|
||||||
final bareSender = event.fromJid.toBare().toString();
|
|
||||||
if (msg.sender.split('/').first != bareSender) {
|
|
||||||
_log.warning(
|
_log.warning(
|
||||||
'Received a message correction from $bareSender for message that is not sent by $bareSender',
|
'Received a message correction from ${event.from} for a message that is sent by ${msg.sender}',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1097,9 +1115,10 @@ class XmppService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(Unknown): Should we null-check here?
|
||||||
final newMsg = await ms.updateMessage(
|
final newMsg = await ms.updateMessage(
|
||||||
msg.id,
|
msg.id,
|
||||||
body: event.body,
|
body: event.extensions.get<MessageBodyData>()!.body,
|
||||||
isEdited: true,
|
isEdited: true,
|
||||||
);
|
);
|
||||||
sendEvent(MessageUpdatedEvent(message: newMsg));
|
sendEvent(MessageUpdatedEvent(message: newMsg));
|
||||||
@ -1120,30 +1139,32 @@ class XmppService {
|
|||||||
) async {
|
) async {
|
||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
// TODO(Unknown): Once we support groupchats, we need to instead query by the stanza-id
|
// TODO(Unknown): Once we support groupchats, we need to instead query by the stanza-id
|
||||||
|
final reactions = event.extensions.get<MessageReactionsData>()!;
|
||||||
final msg = await ms.getMessageByXmppId(
|
final msg = await ms.getMessageByXmppId(
|
||||||
event.messageReactions!.messageId,
|
reactions.messageId,
|
||||||
conversationJid,
|
conversationJid,
|
||||||
queryReactionPreview: false,
|
queryReactionPreview: false,
|
||||||
);
|
);
|
||||||
if (msg == null) {
|
if (msg == null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
'Received reactions for ${event.messageReactions!.messageId} from ${event.fromJid} for $conversationJid, but could not find message.',
|
'Received reactions for ${reactions.messageId} from ${event.from} for $conversationJid, but could not find message.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await GetIt.I.get<ReactionsService>().processNewReactions(
|
await GetIt.I.get<ReactionsService>().processNewReactions(
|
||||||
msg,
|
msg,
|
||||||
event.fromJid.toBare().toString(),
|
event.from.toBare().toString(),
|
||||||
event.messageReactions!.emojis,
|
reactions.emojis,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onMessage(MessageEvent event, {dynamic extra}) async {
|
Future<void> _onMessage(MessageEvent event, {dynamic extra}) async {
|
||||||
// The jid this message event is meant for
|
// The jid this message event is meant for
|
||||||
final conversationJid = event.isCarbon
|
final isCarbon = event.extensions.get<CarbonsData>()?.isCarbon ?? false;
|
||||||
? event.toJid.toBare().toString()
|
final conversationJid = isCarbon
|
||||||
: event.fromJid.toBare().toString();
|
? event.to.toBare().toString()
|
||||||
|
: event.from.toBare().toString();
|
||||||
|
|
||||||
if (event.type == 'error') {
|
if (event.type == 'error') {
|
||||||
await _handleErrorMessage(event);
|
await _handleErrorMessage(event);
|
||||||
@ -1152,40 +1173,41 @@ class XmppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process the chat state update. Can also be attached to other messages
|
// Process the chat state update. Can also be attached to other messages
|
||||||
if (event.chatState != null) {
|
final chatState = event.extensions.get<ChatState>();
|
||||||
await _onChatState(event.chatState!, conversationJid);
|
if (chatState != null) {
|
||||||
|
await _onChatState(chatState, conversationJid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process message corrections separately
|
// Process message corrections separately
|
||||||
if (event.messageCorrectionId != null) {
|
if (event.extensions.get<LastMessageCorrectionData>() != null) {
|
||||||
await _handleMessageCorrection(event, conversationJid);
|
await _handleMessageCorrection(event, conversationJid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process File Upload Notifications replacements separately
|
// Process File Upload Notifications replacements separately
|
||||||
if (event.funReplacement != null) {
|
if (event.extensions.get<FileUploadNotificationReplacementData>() != null) {
|
||||||
await _handleFileUploadNotificationReplacement(event, conversationJid);
|
await _handleFileUploadNotificationReplacement(event, conversationJid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.messageRetraction != null) {
|
if (event.extensions.get<MessageRetractionData>() != null) {
|
||||||
await _handleMessageRetraction(event, conversationJid);
|
await _handleMessageRetraction(event, conversationJid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle message reactions
|
// Handle message reactions
|
||||||
if (event.messageReactions != null) {
|
if (event.extensions.get<MessageReactionsData>() != null) {
|
||||||
await _handleMessageReactions(event, conversationJid);
|
await _handleMessageReactions(event, conversationJid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the processing here if the event does not describe a displayable message
|
// Stop the processing here if the event does not describe a displayable message
|
||||||
if (!_isMessageEventMessage(event) &&
|
if (!_isMessageEventMessage(event) && event.encryptionError == null) return;
|
||||||
event.other['encryption_error'] == null) return;
|
if (event.encryptionError is InvalidKeyExchangeException) return;
|
||||||
if (event.other['encryption_error'] is InvalidKeyExchangeException) return;
|
|
||||||
|
|
||||||
// Ignore File Upload Notifications where we don't have a filename.
|
// Ignore File Upload Notifications where we don't have a filename.
|
||||||
if (event.fun != null && event.fun!.name == null) {
|
final fun = event.extensions.get<FileUploadNotificationData>();
|
||||||
|
if (fun != null && fun.metadata.name == null) {
|
||||||
_log.finest(
|
_log.finest(
|
||||||
'Ignoring File Upload Notification as it does not specify a filename',
|
'Ignoring File Upload Notification as it does not specify a filename',
|
||||||
);
|
);
|
||||||
@ -1200,25 +1222,30 @@ class XmppService {
|
|||||||
// Is the conversation partner in our roster
|
// Is the conversation partner in our roster
|
||||||
final isInRoster = rosterItem != null;
|
final isInRoster = rosterItem != null;
|
||||||
// True if the message was sent by us (via a Carbon)
|
// True if the message was sent by us (via a Carbon)
|
||||||
final sent =
|
final sent = isCarbon && event.from.toBare().toString() == state.jid;
|
||||||
event.isCarbon && event.fromJid.toBare().toString() == state.jid;
|
|
||||||
// The timestamp at which we received the message
|
// The timestamp at which we received the message
|
||||||
final messageTimestamp = DateTime.now().millisecondsSinceEpoch;
|
final messageTimestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
// Acknowledge the message if enabled
|
// Acknowledge the message if enabled
|
||||||
if (event.deliveryReceiptRequested && isInRoster && prefs.sendChatMarkers) {
|
final receiptRequested =
|
||||||
|
event.extensions.get<MessageDeliveryReceiptData>()?.receiptRequested ??
|
||||||
|
false;
|
||||||
|
if (receiptRequested && isInRoster && prefs.sendChatMarkers) {
|
||||||
// NOTE: We do not await it to prevent us being blocked if the IQ response id delayed
|
// NOTE: We do not await it to prevent us being blocked if the IQ response id delayed
|
||||||
await _acknowledgeMessage(event);
|
await _acknowledgeMessage(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-process the message in case it is a reply to another message
|
// Pre-process the message in case it is a reply to another message
|
||||||
|
// TODO(Unknown): Fix the notion that no body means body == ''
|
||||||
|
final body = event.extensions.get<MessageBodyData>()?.body ?? '';
|
||||||
|
final reply = event.extensions.get<ReplyData>();
|
||||||
String? replyId;
|
String? replyId;
|
||||||
var messageBody = event.body;
|
var messageBody = body;
|
||||||
if (event.reply != null) {
|
if (reply != null) {
|
||||||
replyId = event.reply!.id;
|
replyId = reply.id;
|
||||||
|
|
||||||
// Strip the compatibility fallback, if specified
|
// Strip the compatibility fallback, if specified
|
||||||
messageBody = event.reply!.removeFallback(messageBody);
|
messageBody = reply.withoutFallback ?? body;
|
||||||
_log.finest('Removed message reply compatibility fallback from message');
|
_log.finest('Removed message reply compatibility fallback from message');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1261,18 +1288,21 @@ class XmppService {
|
|||||||
var message = await ms.addMessageFromData(
|
var message = await ms.addMessageFromData(
|
||||||
messageBody,
|
messageBody,
|
||||||
messageTimestamp,
|
messageTimestamp,
|
||||||
event.fromJid.toString(),
|
event.from.toString(),
|
||||||
conversationJid,
|
conversationJid,
|
||||||
event.sid,
|
event.id,
|
||||||
event.fun != null,
|
fun != null,
|
||||||
event.encrypted,
|
event.encrypted,
|
||||||
event.messageProcessingHints?.contains(MessageProcessingHint.noStore) ??
|
event.extensions
|
||||||
|
.get<MessageProcessingHintData>()
|
||||||
|
?.hints
|
||||||
|
.contains(MessageProcessingHint.noStore) ??
|
||||||
false,
|
false,
|
||||||
fileMetadata: fileMetadata?.fileMetadata,
|
fileMetadata: fileMetadata?.fileMetadata,
|
||||||
quoteId: replyId,
|
quoteId: replyId,
|
||||||
originId: event.originId,
|
originId: event.extensions.get<StableIdData>()?.originId,
|
||||||
errorType: errorTypeFromException(event.other['encryption_error']),
|
errorType: errorTypeFromException(event.encryptionError),
|
||||||
stickerPackId: event.stickerPackId,
|
stickerPackId: event.extensions.get<StickersData>()?.stickerPackId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Attempt to auto-download the embedded file, if
|
// Attempt to auto-download the embedded file, if
|
||||||
@ -1336,7 +1366,7 @@ class XmppService {
|
|||||||
rosterItem?.title ?? conversationJid.split('@')[0],
|
rosterItem?.title ?? conversationJid.split('@')[0],
|
||||||
message,
|
message,
|
||||||
ConversationType.chat,
|
ConversationType.chat,
|
||||||
rosterItem?.avatarUrl ?? '',
|
rosterItem?.avatarPath ?? '',
|
||||||
conversationJid,
|
conversationJid,
|
||||||
sent ? 0 : 1,
|
sent ? 0 : 1,
|
||||||
messageTimestamp,
|
messageTimestamp,
|
||||||
@ -1392,7 +1422,7 @@ class XmppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mark the file as downlading when it includes a File Upload Notification
|
// Mark the file as downlading when it includes a File Upload Notification
|
||||||
if (event.fun != null) {
|
if (fun != null) {
|
||||||
message = await ms.updateMessage(
|
message = await ms.updateMessage(
|
||||||
message.id,
|
message.id,
|
||||||
isDownloading: true,
|
isDownloading: true,
|
||||||
@ -1408,8 +1438,10 @@ class XmppService {
|
|||||||
String conversationJid,
|
String conversationJid,
|
||||||
) async {
|
) async {
|
||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
var message =
|
|
||||||
await ms.getMessageByStanzaId(conversationJid, event.funReplacement!);
|
final replacementId =
|
||||||
|
event.extensions.get<FileUploadNotificationReplacementData>()!.id;
|
||||||
|
var message = await ms.getMessageByStanzaId(conversationJid, replacementId);
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
'Received a FileUploadNotification replacement for unknown message',
|
'Received a FileUploadNotification replacement for unknown message',
|
||||||
@ -1426,11 +1458,9 @@ class XmppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the Jid is allowed to do so
|
// Check if the Jid is allowed to do so
|
||||||
// TODO(Unknown): Maybe use the JID parser?
|
if (message.senderJid != event.from.toBare()) {
|
||||||
final bareSender = event.fromJid.toBare().toString();
|
|
||||||
if (message.sender.split('/').first != bareSender) {
|
|
||||||
_log.warning(
|
_log.warning(
|
||||||
'Received a FileUploadNotification replacement by $bareSender for message that is not sent by $bareSender',
|
'Received a FileUploadNotification replacement by ${event.from} for a message that is sent by ${message.sender}',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1454,8 +1484,8 @@ class XmppService {
|
|||||||
fileMetadata: fileMetadata ?? notSpecified,
|
fileMetadata: fileMetadata ?? notSpecified,
|
||||||
isFileUploadNotification: false,
|
isFileUploadNotification: false,
|
||||||
isDownloading: shouldDownload,
|
isDownloading: shouldDownload,
|
||||||
sid: event.sid,
|
sid: event.id,
|
||||||
originId: event.originId,
|
originId: event.extensions.get<StableIdData>()?.originId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove the old entry
|
// Remove the old entry
|
||||||
@ -1493,7 +1523,7 @@ class XmppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAvatarUpdated(
|
Future<void> _onAvatarUpdated(
|
||||||
AvatarUpdatedEvent event, {
|
UserAvatarUpdatedEvent event, {
|
||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
await GetIt.I.get<AvatarService>().handleAvatarUpdate(event);
|
await GetIt.I.get<AvatarService>().handleAvatarUpdate(event);
|
||||||
|
10
lib/shared/debug.dart
Normal file
10
lib/shared/debug.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
enum DebugCommand {
|
||||||
|
/// Clear the stream resumption state so that the next connection is fresh.
|
||||||
|
clearStreamResumption(0),
|
||||||
|
requestRoster(1);
|
||||||
|
|
||||||
|
const DebugCommand(this.id);
|
||||||
|
|
||||||
|
/// The id of the command
|
||||||
|
final int id;
|
||||||
|
}
|
@ -14,11 +14,11 @@ class ConversationChatStateConverter
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
ChatState fromJson(Map<String, dynamic> json) =>
|
ChatState fromJson(Map<String, dynamic> json) =>
|
||||||
chatStateFromString(json['chatState'] as String);
|
ChatState.fromName(json['chatState'] as String);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson(ChatState state) => <String, String>{
|
Map<String, dynamic> toJson(ChatState state) => <String, String>{
|
||||||
'chatState': chatStateToString(state),
|
'chatState': state.toName(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,30 +49,52 @@ enum ConversationType {
|
|||||||
@freezed
|
@freezed
|
||||||
class Conversation with _$Conversation {
|
class Conversation with _$Conversation {
|
||||||
factory Conversation(
|
factory Conversation(
|
||||||
|
/// The title of the chat.
|
||||||
String title,
|
String title,
|
||||||
|
|
||||||
|
// The newest message in the chat.
|
||||||
@ConversationMessageConverter() Message? lastMessage,
|
@ConversationMessageConverter() Message? lastMessage,
|
||||||
String avatarUrl,
|
|
||||||
|
// The path to the avatar.
|
||||||
|
String avatarPath,
|
||||||
|
|
||||||
|
// The hash of the avatar.
|
||||||
|
String? avatarHash,
|
||||||
|
|
||||||
|
// The JID of the entity we're having a chat with...
|
||||||
String jid,
|
String jid,
|
||||||
|
|
||||||
|
// The number of unread messages.
|
||||||
int unreadCounter,
|
int unreadCounter,
|
||||||
|
|
||||||
|
// The kind of chat this conversation is representing.
|
||||||
ConversationType type,
|
ConversationType type,
|
||||||
|
|
||||||
|
// The timestamp the conversation was last changed.
|
||||||
// NOTE: In milliseconds since Epoch or -1 if none has ever happened
|
// NOTE: In milliseconds since Epoch or -1 if none has ever happened
|
||||||
int lastChangeTimestamp,
|
int lastChangeTimestamp,
|
||||||
// Indicates if the conversation should be shown on the homescreen
|
|
||||||
|
// Indicates if the conversation should be shown on the homescreen.
|
||||||
bool open,
|
bool open,
|
||||||
// Indicates, if [jid] is a regular user, if the user is in the roster.
|
|
||||||
bool inRoster,
|
/// Flag indicating whether the "add to roster" button should be shown.
|
||||||
// The subscription state of the roster item
|
bool showAddToRoster,
|
||||||
String subscription,
|
|
||||||
// Whether the chat is muted (true = muted, false = not muted)
|
// Whether the chat is muted (true = muted, false = not muted)
|
||||||
bool muted,
|
bool muted,
|
||||||
|
|
||||||
// Whether the conversation is encrypted or not (true = encrypted, false = unencrypted)
|
// Whether the conversation is encrypted or not (true = encrypted, false = unencrypted)
|
||||||
bool encrypted,
|
bool encrypted,
|
||||||
|
|
||||||
// The current chat state
|
// The current chat state
|
||||||
@ConversationChatStateConverter() ChatState chatState, {
|
@ConversationChatStateConverter() ChatState chatState, {
|
||||||
|
|
||||||
// The id of the contact in the device's phonebook if it exists
|
// The id of the contact in the device's phonebook if it exists
|
||||||
String? contactId,
|
String? contactId,
|
||||||
|
|
||||||
// The path to the contact avatar, if available
|
// The path to the contact avatar, if available
|
||||||
String? contactAvatarPath,
|
String? contactAvatarPath,
|
||||||
|
|
||||||
// The contact's display name, if it exists
|
// The contact's display name, if it exists
|
||||||
String? contactDisplayName,
|
String? contactDisplayName,
|
||||||
}) = _Conversation;
|
}) = _Conversation;
|
||||||
@ -85,16 +107,14 @@ class Conversation with _$Conversation {
|
|||||||
|
|
||||||
factory Conversation.fromDatabaseJson(
|
factory Conversation.fromDatabaseJson(
|
||||||
Map<String, dynamic> json,
|
Map<String, dynamic> json,
|
||||||
bool inRoster,
|
bool showAddToRoster,
|
||||||
String subscription,
|
|
||||||
Message? lastMessage,
|
Message? lastMessage,
|
||||||
) {
|
) {
|
||||||
return Conversation.fromJson({
|
return Conversation.fromJson({
|
||||||
...json,
|
...json,
|
||||||
'muted': intToBool(json['muted']! as int),
|
'muted': intToBool(json['muted']! as int),
|
||||||
'open': intToBool(json['open']! as int),
|
'open': intToBool(json['open']! as int),
|
||||||
'inRoster': inRoster,
|
'showAddToRoster': showAddToRoster,
|
||||||
'subscription': subscription,
|
|
||||||
'encrypted': intToBool(json['encrypted']! as int),
|
'encrypted': intToBool(json['encrypted']! as int),
|
||||||
'chatState':
|
'chatState':
|
||||||
const ConversationChatStateConverter().toJson(ChatState.gone),
|
const ConversationChatStateConverter().toJson(ChatState.gone),
|
||||||
@ -107,8 +127,7 @@ class Conversation with _$Conversation {
|
|||||||
final map = toJson()
|
final map = toJson()
|
||||||
..remove('id')
|
..remove('id')
|
||||||
..remove('chatState')
|
..remove('chatState')
|
||||||
..remove('inRoster')
|
..remove('showAddToRoster')
|
||||||
..remove('subscription')
|
|
||||||
..remove('lastMessage');
|
..remove('lastMessage');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -128,10 +147,10 @@ class Conversation with _$Conversation {
|
|||||||
/// XMPP avatar's path.
|
/// XMPP avatar's path.
|
||||||
String? get avatarPathWithOptionalContact {
|
String? get avatarPathWithOptionalContact {
|
||||||
if (GetIt.I.get<PreferencesBloc>().state.enableContactIntegration) {
|
if (GetIt.I.get<PreferencesBloc>().state.enableContactIntegration) {
|
||||||
return contactAvatarPath ?? avatarUrl;
|
return contactAvatarPath ?? avatarPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return avatarUrl;
|
return avatarPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The title of the chat. This returns, if enabled, first the contact's display
|
/// The title of the chat. This returns, if enabled, first the contact's display
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:moxxyv2/service/database/helpers.dart';
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/error_types.dart';
|
import 'package:moxxyv2/shared/error_types.dart';
|
||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
@ -201,9 +202,12 @@ class Message with _$Message {
|
|||||||
/// Returns true if the message can be copied to the clipboard.
|
/// Returns true if the message can be copied to the clipboard.
|
||||||
bool get isCopyable => !isMedia && body.isNotEmpty && !isPseudoMessage;
|
bool get isCopyable => !isMedia && body.isNotEmpty && !isPseudoMessage;
|
||||||
|
|
||||||
/// Returns true if the message is a sticker
|
/// Returns true if the message is a sticker.
|
||||||
bool get isSticker => isMedia && stickerPackId != null && !isPseudoMessage;
|
bool get isSticker => isMedia && stickerPackId != null && !isPseudoMessage;
|
||||||
|
|
||||||
/// True if the message is a media message
|
/// True if the message is a media message.
|
||||||
bool get isMedia => fileMetadata != null;
|
bool get isMedia => fileMetadata != null;
|
||||||
|
|
||||||
|
/// The JID of the sender in moxxmpp's format.
|
||||||
|
JID get senderJid => JID.fromString(sender);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ part 'roster.g.dart';
|
|||||||
class RosterItem with _$RosterItem {
|
class RosterItem with _$RosterItem {
|
||||||
factory RosterItem(
|
factory RosterItem(
|
||||||
int id,
|
int id,
|
||||||
String avatarUrl,
|
String avatarPath,
|
||||||
String avatarHash,
|
String avatarHash,
|
||||||
String jid,
|
String jid,
|
||||||
String title,
|
String title,
|
||||||
@ -53,4 +53,24 @@ class RosterItem with _$RosterItem {
|
|||||||
'pseudoRosterItem': boolToInt(pseudoRosterItem),
|
'pseudoRosterItem': boolToInt(pseudoRosterItem),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a conversation with this roster item should display the "Add to roster" button.
|
||||||
|
bool get showAddToRosterButton {
|
||||||
|
// Those chats are not dealt with on the roster
|
||||||
|
if (pseudoRosterItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A full presence subscription is already achieved. Nothing to do
|
||||||
|
if (subscription == 'both') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are not yet waiting for a response to the presence request
|
||||||
|
if (ask == 'subscribe' && ['none', 'from', 'to'].contains(subscription)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ class AddContactBloc extends Bloc<AddContactEvent, AddContactState> {
|
|||||||
RequestedConversationEvent(
|
RequestedConversationEvent(
|
||||||
result.conversation!.jid,
|
result.conversation!.jid,
|
||||||
result.conversation!.title,
|
result.conversation!.title,
|
||||||
result.conversation!.avatarUrl,
|
result.conversation!.avatarPath,
|
||||||
removeUntilConversations: true,
|
removeUntilConversations: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -102,7 +102,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
conversation: state.conversation!.copyWith(
|
conversation: state.conversation!.copyWith(
|
||||||
inRoster: true,
|
showAddToRoster: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -53,7 +53,7 @@ class ConversationsBloc extends Bloc<ConversationsEvent, ConversationsState> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
displayName: event.displayName,
|
displayName: event.displayName,
|
||||||
jid: event.jid,
|
jid: event.jid,
|
||||||
avatarUrl: event.avatarUrl ?? '',
|
avatarPath: event.avatarUrl ?? '',
|
||||||
conversations: event.conversations..sort(compareConversation),
|
conversations: event.conversations..sort(compareConversation),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -118,7 +118,7 @@ class ConversationsBloc extends Bloc<ConversationsEvent, ConversationsState> {
|
|||||||
) async {
|
) async {
|
||||||
return emit(
|
return emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
avatarUrl: event.path,
|
avatarPath: event.path,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ class ConversationsState with _$ConversationsState {
|
|||||||
factory ConversationsState({
|
factory ConversationsState({
|
||||||
@Default(<Conversation>[]) List<Conversation> conversations,
|
@Default(<Conversation>[]) List<Conversation> conversations,
|
||||||
@Default('') String displayName,
|
@Default('') String displayName,
|
||||||
@Default('') String avatarUrl,
|
@Default('') String avatarPath,
|
||||||
@Default('') String jid,
|
@Default('') String jid,
|
||||||
}) = _ConversationsState;
|
}) = _ConversationsState;
|
||||||
}
|
}
|
||||||
|
@ -90,16 +90,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
|
|||||||
SetSubscriptionStateEvent event,
|
SetSubscriptionStateEvent event,
|
||||||
Emitter<ProfileState> emit,
|
Emitter<ProfileState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
conversation: state.conversation!.copyWith(
|
|
||||||
// NOTE: This is wrong, but we just keep it like this until the real result comes
|
|
||||||
// in.
|
|
||||||
subscription: event.shareStatus ? 'to' : 'from',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
SetShareOnlineStatusCommand(jid: event.jid, share: event.shareStatus),
|
SetShareOnlineStatusCommand(jid: event.jid, share: event.shareStatus),
|
||||||
awaitable: false,
|
awaitable: false,
|
||||||
|
@ -28,6 +28,7 @@ enum ShareSelectionType {
|
|||||||
class ShareListItem {
|
class ShareListItem {
|
||||||
const ShareListItem(
|
const ShareListItem(
|
||||||
this.avatarPath,
|
this.avatarPath,
|
||||||
|
this.avatarHash,
|
||||||
this.jid,
|
this.jid,
|
||||||
this.title,
|
this.title,
|
||||||
this.isConversation,
|
this.isConversation,
|
||||||
@ -38,6 +39,7 @@ class ShareListItem {
|
|||||||
this.contactDisplayName,
|
this.contactDisplayName,
|
||||||
);
|
);
|
||||||
final String avatarPath;
|
final String avatarPath;
|
||||||
|
final String? avatarHash;
|
||||||
final String jid;
|
final String jid;
|
||||||
final String title;
|
final String title;
|
||||||
final bool isConversation;
|
final bool isConversation;
|
||||||
@ -79,7 +81,8 @@ class ShareSelectionBloc
|
|||||||
final items = List<ShareListItem>.from(
|
final items = List<ShareListItem>.from(
|
||||||
conversations.map((c) {
|
conversations.map((c) {
|
||||||
return ShareListItem(
|
return ShareListItem(
|
||||||
c.avatarUrl,
|
c.avatarPath,
|
||||||
|
c.avatarHash,
|
||||||
c.jid,
|
c.jid,
|
||||||
c.title,
|
c.title,
|
||||||
true,
|
true,
|
||||||
@ -100,7 +103,8 @@ class ShareSelectionBloc
|
|||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
items.add(
|
items.add(
|
||||||
ShareListItem(
|
ShareListItem(
|
||||||
rosterItem.avatarUrl,
|
rosterItem.avatarPath,
|
||||||
|
rosterItem.avatarHash,
|
||||||
rosterItem.jid,
|
rosterItem.jid,
|
||||||
rosterItem.title,
|
rosterItem.title,
|
||||||
false,
|
false,
|
||||||
@ -113,7 +117,8 @@ class ShareSelectionBloc
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
items[index] = ShareListItem(
|
items[index] = ShareListItem(
|
||||||
rosterItem.avatarUrl,
|
rosterItem.avatarPath,
|
||||||
|
rosterItem.avatarHash,
|
||||||
rosterItem.jid,
|
rosterItem.jid,
|
||||||
rosterItem.title,
|
rosterItem.title,
|
||||||
false,
|
false,
|
||||||
@ -187,7 +192,7 @@ class ShareSelectionBloc
|
|||||||
SendMessageCommand(
|
SendMessageCommand(
|
||||||
recipients: _getRecipients(),
|
recipients: _getRecipients(),
|
||||||
body: state.text!,
|
body: state.text!,
|
||||||
chatState: chatStateToString(ChatState.gone),
|
chatState: ChatState.gone.toName(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -324,7 +324,7 @@ class BidirectionalConversationController
|
|||||||
recipients: [conversationJid],
|
recipients: [conversationJid],
|
||||||
body: text,
|
body: text,
|
||||||
quotedMessage: _quotedMessage,
|
quotedMessage: _quotedMessage,
|
||||||
chatState: chatStateToString(ChatState.active),
|
chatState: ChatState.active.toName(),
|
||||||
editId: _messageEditingState?.id,
|
editId: _messageEditingState?.id,
|
||||||
editSid: _messageEditingState?.sid,
|
editSid: _messageEditingState?.sid,
|
||||||
currentConversationJid: conversationJid,
|
currentConversationJid: conversationJid,
|
||||||
|
@ -17,6 +17,7 @@ import 'package:moxxyv2/ui/bloc/profile_bloc.dart' as profile;
|
|||||||
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart' as stickers;
|
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart' as stickers;
|
||||||
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
||||||
import 'package:moxxyv2/ui/prestart.dart';
|
import 'package:moxxyv2/ui/prestart.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/avatars.dart';
|
||||||
import 'package:moxxyv2/ui/service/progress.dart';
|
import 'package:moxxyv2/ui/service/progress.dart';
|
||||||
|
|
||||||
void setupEventHandler() {
|
void setupEventHandler() {
|
||||||
@ -34,6 +35,9 @@ void setupEventHandler() {
|
|||||||
EventTypeMatcher<ServiceReadyEvent>(onServiceReady),
|
EventTypeMatcher<ServiceReadyEvent>(onServiceReady),
|
||||||
EventTypeMatcher<MessageNotificationTappedEvent>(onNotificationTappend),
|
EventTypeMatcher<MessageNotificationTappedEvent>(onNotificationTappend),
|
||||||
EventTypeMatcher<StickerPackAddedEvent>(onStickerPackAdded),
|
EventTypeMatcher<StickerPackAddedEvent>(onStickerPackAdded),
|
||||||
|
EventTypeMatcher<StreamNegotiationsCompletedEvent>(
|
||||||
|
onStreamNegotiationsDone,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
GetIt.I.registerSingleton<EventHandler>(handler);
|
GetIt.I.registerSingleton<EventHandler>(handler);
|
||||||
@ -173,7 +177,7 @@ Future<void> onNotificationTappend(
|
|||||||
conversation.RequestedConversationEvent(
|
conversation.RequestedConversationEvent(
|
||||||
event.conversationJid,
|
event.conversationJid,
|
||||||
event.title,
|
event.title,
|
||||||
event.avatarUrl,
|
event.avatarPath,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -186,3 +190,12 @@ Future<void> onStickerPackAdded(
|
|||||||
stickers.StickerPackAddedEvent(event.stickerPack),
|
stickers.StickerPackAddedEvent(event.stickerPack),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> onStreamNegotiationsDone(
|
||||||
|
StreamNegotiationsCompletedEvent event, {
|
||||||
|
dynamic extra,
|
||||||
|
}) async {
|
||||||
|
if (!event.resumed) {
|
||||||
|
GetIt.I.get<UIAvatarsService>().resetCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -448,9 +448,12 @@ class ConversationPageState extends State<ConversationPage>
|
|||||||
children: [
|
children: [
|
||||||
BlocBuilder<ConversationBloc, ConversationState>(
|
BlocBuilder<ConversationBloc, ConversationState>(
|
||||||
buildWhen: (prev, next) =>
|
buildWhen: (prev, next) =>
|
||||||
prev.conversation?.inRoster != next.conversation?.inRoster,
|
prev.conversation?.showAddToRoster !=
|
||||||
|
next.conversation?.showAddToRoster,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if ((state.conversation?.inRoster ?? false) ||
|
final showAddToRoster =
|
||||||
|
state.conversation?.showAddToRoster ?? false;
|
||||||
|
if (!showAddToRoster ||
|
||||||
state.conversation?.type == ConversationType.note) {
|
state.conversation?.type == ConversationType.note) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ class ConversationTopbar extends StatelessWidget
|
|||||||
|
|
||||||
bool _shouldRebuild(ConversationState prev, ConversationState next) {
|
bool _shouldRebuild(ConversationState prev, ConversationState next) {
|
||||||
return prev.conversation?.title != next.conversation?.title ||
|
return prev.conversation?.title != next.conversation?.title ||
|
||||||
prev.conversation?.avatarUrl != next.conversation?.avatarUrl ||
|
prev.conversation?.avatarPath != next.conversation?.avatarPath ||
|
||||||
prev.conversation?.chatState != next.conversation?.chatState ||
|
prev.conversation?.chatState != next.conversation?.chatState ||
|
||||||
prev.conversation?.jid != next.conversation?.jid ||
|
prev.conversation?.jid != next.conversation?.jid ||
|
||||||
prev.conversation?.encrypted != next.conversation?.encrypted;
|
prev.conversation?.encrypted != next.conversation?.encrypted;
|
||||||
@ -110,14 +110,17 @@ class ConversationTopbar extends StatelessWidget
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: RebuildOnContactIntegrationChange(
|
child: RebuildOnContactIntegrationChange(
|
||||||
builder: () => AvatarWrapper(
|
builder: () => CachingXMPPAvatar(
|
||||||
radius: 25,
|
jid: state.conversation?.jid ?? '',
|
||||||
avatarUrl: state.conversation
|
|
||||||
?.avatarPathWithOptionalContact ??
|
|
||||||
'',
|
|
||||||
altText: state
|
altText: state
|
||||||
.conversation?.titleWithOptionalContact ??
|
.conversation?.titleWithOptionalContact ??
|
||||||
'A',
|
'A',
|
||||||
|
radius: 25,
|
||||||
|
hasContactId:
|
||||||
|
state.conversation?.contactId != null,
|
||||||
|
hash: state.conversation?.avatarHash,
|
||||||
|
path: state.conversation?.avatarPath,
|
||||||
|
shouldRequest: state.conversation != null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -158,7 +158,7 @@ class ConversationsPageState extends State<ConversationsPage>
|
|||||||
RequestedConversationEvent(
|
RequestedConversationEvent(
|
||||||
item.jid,
|
item.jid,
|
||||||
item.title,
|
item.title,
|
||||||
item.avatarUrl,
|
item.avatarPath,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
key: key,
|
key: key,
|
||||||
@ -251,7 +251,7 @@ class ConversationsPageState extends State<ConversationsPage>
|
|||||||
profile.ProfilePageRequestedEvent(
|
profile.ProfilePageRequestedEvent(
|
||||||
true,
|
true,
|
||||||
jid: state.jid,
|
jid: state.jid,
|
||||||
avatarUrl: state.avatarUrl,
|
avatarUrl: state.avatarPath,
|
||||||
displayName: state.displayName,
|
displayName: state.displayName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -265,10 +265,17 @@ class ConversationsPageState extends State<ConversationsPage>
|
|||||||
tag: 'self_profile_picture',
|
tag: 'self_profile_picture',
|
||||||
child: Material(
|
child: Material(
|
||||||
color: const Color.fromRGBO(0, 0, 0, 0),
|
color: const Color.fromRGBO(0, 0, 0, 0),
|
||||||
child: AvatarWrapper(
|
// NOTE: We do not care about the avatar hash because
|
||||||
|
// we just read it from the XMPP state in the
|
||||||
|
// avatar service.
|
||||||
|
child: CachingXMPPAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
avatarUrl: state.avatarUrl,
|
path: state.avatarPath,
|
||||||
|
altText: state.jid[0],
|
||||||
altIcon: Icons.person,
|
altIcon: Icons.person,
|
||||||
|
hasContactId: false,
|
||||||
|
jid: state.jid,
|
||||||
|
ownAvatar: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -108,14 +108,14 @@ class NewConversationPage extends StatelessWidget {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
item.avatarUrl,
|
item.avatarPath,
|
||||||
|
item.avatarHash,
|
||||||
item.jid,
|
item.jid,
|
||||||
0,
|
0,
|
||||||
ConversationType.chat,
|
ConversationType.chat,
|
||||||
0,
|
0,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
'',
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
ChatState.gone,
|
ChatState.gone,
|
||||||
@ -130,7 +130,7 @@ class NewConversationPage extends StatelessWidget {
|
|||||||
NewConversationAddedEvent(
|
NewConversationAddedEvent(
|
||||||
item.jid,
|
item.jid,
|
||||||
item.title,
|
item.title,
|
||||||
item.avatarUrl,
|
item.avatarPath,
|
||||||
ConversationType.chat,
|
ConversationType.chat,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
|
import 'package:moxxyv2/shared/debug.dart' as debug;
|
||||||
import 'package:moxxyv2/shared/models/preferences.dart';
|
import 'package:moxxyv2/shared/models/preferences.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
@ -148,6 +151,28 @@ class DebuggingPage extends StatelessWidget {
|
|||||||
...kDebugMode
|
...kDebugMode
|
||||||
? [
|
? [
|
||||||
const SectionTitle('Testing'),
|
const SectionTitle('Testing'),
|
||||||
|
SettingsRow(
|
||||||
|
title: 'Reset stream management state',
|
||||||
|
onTap: () {
|
||||||
|
MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
|
DebugCommand(
|
||||||
|
id: debug.DebugCommand.clearStreamResumption.id,
|
||||||
|
),
|
||||||
|
awaitable: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SettingsRow(
|
||||||
|
title: 'Request roster',
|
||||||
|
onTap: () {
|
||||||
|
MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
|
DebugCommand(
|
||||||
|
id: debug.DebugCommand.requestRoster.id,
|
||||||
|
),
|
||||||
|
awaitable: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
title: 'Reset showDebugMenu state',
|
title: 'Reset showDebugMenu state',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -76,13 +76,13 @@ class ShareSelectionPage extends StatelessWidget {
|
|||||||
item.title,
|
item.title,
|
||||||
null,
|
null,
|
||||||
item.avatarPath,
|
item.avatarPath,
|
||||||
|
item.avatarHash,
|
||||||
item.jid,
|
item.jid,
|
||||||
0,
|
0,
|
||||||
ConversationType.chat,
|
ConversationType.chat,
|
||||||
0,
|
0,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
'',
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
ChatState.gone,
|
ChatState.gone,
|
||||||
|
30
lib/ui/service/avatars.dart
Normal file
30
lib/ui/service/avatars.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
|
|
||||||
|
class UIAvatarsService {
|
||||||
|
/// Logger
|
||||||
|
final Logger _log = Logger('UIAvatarsService');
|
||||||
|
|
||||||
|
/// Mapping between a JID and whether we have requested an avatar for the
|
||||||
|
/// JID already in the session (from login until stream resumption failure).
|
||||||
|
final Map<String, bool> _avatarRequested = {};
|
||||||
|
|
||||||
|
void requestAvatarIfRequired(String jid, String? hash, bool ownAvatar) {
|
||||||
|
if (_avatarRequested[jid] ?? false) return;
|
||||||
|
|
||||||
|
_log.finest('Requesting avatar for $jid');
|
||||||
|
_avatarRequested[jid] = true;
|
||||||
|
MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
|
RequestAvatarForJidCommand(
|
||||||
|
jid: jid,
|
||||||
|
hash: hash,
|
||||||
|
ownAvatar: ownAvatar,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetCache() {
|
||||||
|
_avatarRequested.clear();
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/helpers.dart';
|
import 'package:moxxyv2/ui/helpers.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/avatars.dart';
|
||||||
import 'package:moxxyv2/ui/theme.dart';
|
import 'package:moxxyv2/ui/theme.dart';
|
||||||
|
|
||||||
class AvatarWrapper extends StatelessWidget {
|
class AvatarWrapper extends StatelessWidget {
|
||||||
@ -99,3 +102,90 @@ class AvatarWrapper extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CachingXMPPAvatar extends StatefulWidget {
|
||||||
|
const CachingXMPPAvatar({
|
||||||
|
required this.jid,
|
||||||
|
required this.altText,
|
||||||
|
required this.radius,
|
||||||
|
required this.hasContactId,
|
||||||
|
this.altIcon,
|
||||||
|
this.shouldRequest = true,
|
||||||
|
this.ownAvatar = false,
|
||||||
|
this.hash,
|
||||||
|
this.path,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The JID of the entity.
|
||||||
|
final String jid;
|
||||||
|
|
||||||
|
/// The hash of the JID's avatar or null, if we don't know of an avatar.
|
||||||
|
final String? hash;
|
||||||
|
|
||||||
|
/// The alt-text, if [path] is null.
|
||||||
|
final String altText;
|
||||||
|
|
||||||
|
/// The (potentially null) path to the avatar image.
|
||||||
|
final String? path;
|
||||||
|
|
||||||
|
/// The radius of the avatar widget.
|
||||||
|
final double radius;
|
||||||
|
|
||||||
|
/// Flag indicating whether the conversation has a contactId != null.
|
||||||
|
final bool hasContactId;
|
||||||
|
|
||||||
|
/// Flag indicating whether a request for the avatar should happen or not.
|
||||||
|
final bool shouldRequest;
|
||||||
|
|
||||||
|
/// Alt-icon, if [path] is null.
|
||||||
|
final IconData? altIcon;
|
||||||
|
|
||||||
|
/// Flag indicating whether this avatar is our own avatar.
|
||||||
|
final bool ownAvatar;
|
||||||
|
|
||||||
|
@override
|
||||||
|
CachingXMPPAvatarState createState() => CachingXMPPAvatarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CachingXMPPAvatarState extends State<CachingXMPPAvatar> {
|
||||||
|
void _performRequest() {
|
||||||
|
// Only request the avatar if we don't have a contact integration avatar already.
|
||||||
|
if (!GetIt.I.get<PreferencesBloc>().state.enableContactIntegration ||
|
||||||
|
!widget.hasContactId) {
|
||||||
|
GetIt.I.get<UIAvatarsService>().requestAvatarIfRequired(
|
||||||
|
widget.jid,
|
||||||
|
widget.hash,
|
||||||
|
widget.ownAvatar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (!widget.shouldRequest) return;
|
||||||
|
|
||||||
|
_performRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(CachingXMPPAvatar oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (widget.shouldRequest && !oldWidget.shouldRequest) {
|
||||||
|
_performRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AvatarWrapper(
|
||||||
|
avatarUrl: widget.path,
|
||||||
|
altText: widget.altText,
|
||||||
|
altIcon: widget.altIcon,
|
||||||
|
radius: widget.radius,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -81,12 +81,12 @@ class ReactionList extends StatelessWidget {
|
|||||||
return ReactionsRow(
|
return ReactionsRow(
|
||||||
avatar: ownReaction
|
avatar: ownReaction
|
||||||
? AvatarWrapper(
|
? AvatarWrapper(
|
||||||
avatarUrl: bloc.state.avatarUrl,
|
avatarUrl: bloc.state.avatarPath,
|
||||||
radius: 35,
|
radius: 35,
|
||||||
altIcon: Icons.person,
|
altIcon: Icons.person,
|
||||||
)
|
)
|
||||||
: AvatarWrapper(
|
: AvatarWrapper(
|
||||||
avatarUrl: conversation?.avatarUrl,
|
avatarUrl: conversation?.avatarPath,
|
||||||
radius: 35,
|
radius: 35,
|
||||||
altIcon: Icons.person,
|
altIcon: Icons.person,
|
||||||
),
|
),
|
||||||
|
@ -180,10 +180,14 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
|||||||
Widget _buildAvatar() {
|
Widget _buildAvatar() {
|
||||||
return RebuildOnContactIntegrationChange(
|
return RebuildOnContactIntegrationChange(
|
||||||
builder: () {
|
builder: () {
|
||||||
final avatar = AvatarWrapper(
|
final avatar = CachingXMPPAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
avatarUrl: widget.conversation.avatarPathWithOptionalContact,
|
jid: widget.conversation.jid,
|
||||||
|
hash: widget.conversation.avatarHash,
|
||||||
altText: widget.conversation.titleWithOptionalContact,
|
altText: widget.conversation.titleWithOptionalContact,
|
||||||
|
path: widget.conversation.avatarPathWithOptionalContact,
|
||||||
|
hasContactId: widget.conversation.contactId != null,
|
||||||
|
shouldRequest: widget.conversation.type != ConversationType.note,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.enableAvatarOnTap &&
|
if (widget.enableAvatarOnTap &&
|
||||||
|
@ -34,13 +34,6 @@
|
|||||||
<xmpp:version>2.5rc3</xmpp:version>
|
<xmpp:version>2.5rc3</xmpp:version>
|
||||||
</xmpp:SupportedXep>
|
</xmpp:SupportedXep>
|
||||||
</implements>
|
</implements>
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html" />
|
|
||||||
<xmpp:status>partial</xmpp:status>
|
|
||||||
<xmpp:version>1.2</xmpp:version>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
<implements>
|
||||||
<xmpp:SupportedXep>
|
<xmpp:SupportedXep>
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html" />
|
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html" />
|
||||||
|
@ -954,10 +954,10 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "packages/moxxmpp"
|
path: "packages/moxxmpp"
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: "1475cb542f7d68824f8f4b11efd751cfb3eea428"
|
resolved-ref: f2d8c6a009d3f08806e1565bf9b92407e659f178
|
||||||
url: "https://codeberg.org/moxxy/moxxmpp.git"
|
url: "https://codeberg.org/moxxy/moxxmpp.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.2"
|
version: "0.4.0"
|
||||||
moxxmpp_socket_tcp:
|
moxxmpp_socket_tcp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -71,7 +71,7 @@ dependencies:
|
|||||||
version: 0.1.15
|
version: 0.1.15
|
||||||
moxxmpp:
|
moxxmpp:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.3.1
|
version: 0.4.0
|
||||||
moxxmpp_socket_tcp:
|
moxxmpp_socket_tcp:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.3.1
|
version: 0.3.1
|
||||||
@ -139,7 +139,7 @@ dependency_overrides:
|
|||||||
moxxmpp:
|
moxxmpp:
|
||||||
git:
|
git:
|
||||||
url: https://codeberg.org/moxxy/moxxmpp.git
|
url: https://codeberg.org/moxxy/moxxmpp.git
|
||||||
rev: 1475cb542f7d68824f8f4b11efd751cfb3eea428
|
rev: f2d8c6a009d3f08806e1565bf9b92407e659f178
|
||||||
path: packages/moxxmpp
|
path: packages/moxxmpp
|
||||||
|
|
||||||
extra_licenses:
|
extra_licenses:
|
||||||
|
19
scripts/check-pr.sh
Normal file
19
scripts/check-pr.sh
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run before merging a PR. Checks if the formatting is correct.
|
||||||
|
# Check if there are any formatting issues
|
||||||
|
echo "Checking formatting..."
|
||||||
|
dart format --output none --set-exit-if-changed . &> /dev/null
|
||||||
|
if [[ ! "$?" = "0" ]]; then
|
||||||
|
echo "Error: dart format indicates that format the code is not formatted properly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the linter has any issues
|
||||||
|
echo "Checking linter..."
|
||||||
|
flutter analyze &> /dev/null
|
||||||
|
if [[ ! "$?" = "0" ]]; then
|
||||||
|
echo "Error: flutter analyze indicates that there are lint issues"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PR looks good!"
|
Loading…
Reference in New Issue
Block a user