Merge branch 'krille/use-hive-collections' into 'main'

refactor: Switch to Hive Collections DB

See merge request famedly/fluffychat!896
This commit is contained in:
Krille Fear 2022-06-09 10:51:44 +00:00
commit 8e2acb2819
29 changed files with 347 additions and 216 deletions

View File

@ -461,11 +461,11 @@ class ChatController extends State<Chat> {
if (selectedEvents.length == 1) { if (selectedEvents.length == 1) {
return selectedEvents.first return selectedEvents.first
.getDisplayEvent(timeline!) .getDisplayEvent(timeline!)
.getLocalizedBody(MatrixLocals(L10n.of(context)!)); .calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!));
} }
for (final event in selectedEvents) { for (final event in selectedEvents) {
if (copyString.isNotEmpty) copyString += '\n\n'; if (copyString.isNotEmpty) copyString += '\n\n';
copyString += event.getDisplayEvent(timeline!).getLocalizedBody( copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true); withSenderNamePrefix: true);
} }
@ -773,7 +773,7 @@ class ChatController extends State<Chat> {
editEvent = selectedEvents.first; editEvent = selectedEvents.first;
inputText = sendController.text = editEvent! inputText = sendController.text = editEvent!
.getDisplayEvent(timeline!) .getDisplayEvent(timeline!)
.getLocalizedBody(MatrixLocals(L10n.of(context)!), .calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, hideReply: true); withSenderNamePrefix: false, hideReply: true);
selectedEvents.clear(); selectedEvents.clear();
}); });

View File

@ -29,10 +29,11 @@ class ChatAppBarTitle extends StatelessWidget {
? () => showModalBottomSheet( ? () => showModalBottomSheet(
context: context, context: context,
builder: (c) => UserBottomSheet( builder: (c) => UserBottomSheet(
user: room.getUserByMXIDSync(directChatMatrixID), user: room
.unsafeGetUserFromMemoryOrFallback(directChatMatrixID),
outerContext: context, outerContext: context,
onMention: () => controller.sendController.text += onMention: () => controller.sendController.text +=
'${room.getUserByMXIDSync(directChatMatrixID).mention} ', '${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ',
), ),
) )
: () => VRouter.of(context).toSegments(['rooms', room.id, 'details']), : () => VRouter.of(context).toSegments(['rooms', room.id, 'details']),

View File

@ -293,14 +293,14 @@ class _ChatAccountPicker extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: controller.sendingClient!.ownProfile, future: controller.sendingClient!.fetchOwnProfile(),
builder: (context, snapshot) => PopupMenuButton<String>( builder: (context, snapshot) => PopupMenuButton<String>(
onSelected: _popupMenuButtonSelected, onSelected: _popupMenuButtonSelected,
itemBuilder: (BuildContext context) => clients itemBuilder: (BuildContext context) => clients
.map((client) => PopupMenuItem<String>( .map((client) => PopupMenuItem<String>(
value: client!.userID, value: client!.userID,
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: client.ownProfile, future: client.fetchOwnProfile(),
builder: (context, snapshot) => ListTile( builder: (context, snapshot) => ListTile(
leading: Avatar( leading: Avatar(
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,

View File

@ -350,12 +350,12 @@ class ChatView extends StatelessWidget {
builder: (c) => builder: (c) =>
UserBottomSheet( UserBottomSheet(
user: event user: event
.sender, .senderFromMemoryOrFallback,
outerContext: outerContext:
context, context,
onMention: () => controller onMention: () => controller
.sendController .sendController
.text += '${event.sender.mention} ', .text += '${event.senderFromMemoryOrFallback.mention} ',
), ),
), ),
unfold: controller unfold: controller

View File

@ -48,12 +48,12 @@ class EventInfoDialog extends StatelessWidget {
children: [ children: [
ListTile( ListTile(
leading: Avatar( leading: Avatar(
mxContent: event.sender.avatarUrl, mxContent: event.senderFromMemoryOrFallback.avatarUrl,
name: event.sender.calcDisplayname(), name: event.senderFromMemoryOrFallback.calcDisplayname(),
), ),
title: Text(L10n.of(context)!.sender), title: Text(L10n.of(context)!.sender),
subtitle: subtitle: Text(
Text('${event.sender.calcDisplayname()} [${event.senderId}]'), '${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]'),
), ),
ListTile( ListTile(
title: Text(L10n.of(context)!.time), title: Text(L10n.of(context)!.time),

View File

@ -75,7 +75,7 @@ class Message extends StatelessWidget {
EventTypes.Sticker, EventTypes.Sticker,
EventTypes.Encrypted, EventTypes.Encrypted,
].contains(nextEvent!.type) ].contains(nextEvent!.type)
? nextEvent!.sender.id == event.sender.id && !displayTime ? nextEvent!.senderId == event.senderId && !displayTime
: false; : false;
final textColor = ownMessage final textColor = ownMessage
? Theme.of(context).colorScheme.onPrimary ? Theme.of(context).colorScheme.onPrimary
@ -125,11 +125,16 @@ class Message extends StatelessWidget {
), ),
), ),
)) ))
: Avatar( : FutureBuilder<User?>(
mxContent: event.sender.avatarUrl, future: event.fetchSenderUser(),
name: event.sender.calcDisplayname(), builder: (context, snapshot) {
onTap: () => onAvatarTab!(event), final user = snapshot.data ?? event.senderFromMemoryOrFallback;
), return Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
onTap: () => onAvatarTab!(event),
);
}),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -140,14 +145,22 @@ class Message extends StatelessWidget {
padding: const EdgeInsets.only(left: 8.0, bottom: 4), padding: const EdgeInsets.only(left: 8.0, bottom: 4),
child: ownMessage || event.room.isDirectChat child: ownMessage || event.room.isDirectChat
? const SizedBox(height: 12) ? const SizedBox(height: 12)
: Text( : FutureBuilder<User?>(
event.sender.calcDisplayname(), future: event.fetchSenderUser(),
style: TextStyle( builder: (context, snapshot) {
fontSize: 12, final displayname =
fontWeight: FontWeight.bold, snapshot.data?.calcDisplayname() ??
color: event.sender.calcDisplayname().color, event.senderFromMemoryOrFallback
), .calcDisplayname();
), return Text(
displayname,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: displayname.color,
),
);
}),
), ),
Container( Container(
alignment: alignment, alignment: alignment,

View File

@ -34,7 +34,7 @@ class MessageContent extends StatelessWidget {
content: Text( content: Text(
event.type == EventTypes.Encrypted event.type == EventTypes.Encrypted
? L10n.of(context)!.needPantalaimonWarning ? L10n.of(context)!.needPantalaimonWarning
: event.getLocalizedBody( : event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
), ),
))); )));
@ -172,48 +172,73 @@ class MessageContent extends StatelessWidget {
textmessage: textmessage:
default: default:
if (event.redacted) { if (event.redacted) {
return _ButtonContent( return FutureBuilder<User?>(
label: L10n.of(context)! future: event.fetchSenderUser(),
.redactedAnEvent(event.sender.calcDisplayname()), builder: (context, snapshot) {
icon: const Icon(Icons.delete_outlined), return _ButtonContent(
textColor: buttonTextColor, label: L10n.of(context)!.redactedAnEvent(snapshot.data
onPressed: () => onInfoTab!(event), ?.calcDisplayname() ??
); event.senderFromMemoryOrFallback.calcDisplayname()),
icon: const Icon(Icons.delete_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
);
});
} }
final bigEmotes = event.onlyEmotes && final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 && event.numberEmotes > 0 &&
event.numberEmotes <= 10; event.numberEmotes <= 10;
return LinkText( return FutureBuilder<String>(
text: event.getLocalizedBody(MatrixLocals(L10n.of(context)!), future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!),
hideReply: true), hideReply: true),
textStyle: TextStyle( builder: (context, snapshot) {
color: textColor, return LinkText(
fontSize: bigEmotes ? fontSize * 3 : fontSize, text: snapshot.data ??
decoration: event.redacted ? TextDecoration.lineThrough : null, event.calcLocalizedBodyFallback(
), MatrixLocals(L10n.of(context)!),
linkStyle: TextStyle( hideReply: true),
color: textColor.withAlpha(150), textStyle: TextStyle(
fontSize: bigEmotes ? fontSize * 3 : fontSize, color: textColor,
decoration: TextDecoration.underline, fontSize: bigEmotes ? fontSize * 3 : fontSize,
), decoration:
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), event.redacted ? TextDecoration.lineThrough : null,
); ),
linkStyle: TextStyle(
color: textColor.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
),
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
);
});
} }
case EventTypes.CallInvite: case EventTypes.CallInvite:
return _ButtonContent( return FutureBuilder<User?>(
label: L10n.of(context)!.startedACall(event.sender.calcDisplayname()), future: event.fetchSenderUser(),
icon: const Icon(Icons.phone_outlined), builder: (context, snapshot) {
textColor: buttonTextColor, return _ButtonContent(
onPressed: () => onInfoTab!(event), label: L10n.of(context)!.startedACall(
); snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback.calcDisplayname()),
icon: const Icon(Icons.phone_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
);
});
default: default:
return _ButtonContent( return FutureBuilder<User?>(
label: L10n.of(context)! future: event.fetchSenderUser(),
.userSentUnknownEvent(event.sender.calcDisplayname(), event.type), builder: (context, snapshot) {
icon: const Icon(Icons.info_outlined), return _ButtonContent(
textColor: buttonTextColor, label: L10n.of(context)!.userSentUnknownEvent(
onPressed: () => onInfoTab!(event), snapshot.data?.calcDisplayname() ??
); event.senderFromMemoryOrFallback.calcDisplayname(),
event.type),
icon: const Icon(Icons.info_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
);
});
} }
} }
} }

View File

@ -39,7 +39,7 @@ class MessageReactions extends StatelessWidget {
); );
} }
reactionMap[key]!.count++; reactionMap[key]!.count++;
reactionMap[key]!.reactors!.add(e.sender); reactionMap[key]!.reactors!.add(e.senderFromMemoryOrFallback);
reactionMap[key]!.reacted |= e.senderId == e.room.client.userID; reactionMap[key]!.reacted |= e.senderId == e.room.client.userID;
} }
} }

View File

@ -52,7 +52,7 @@ class ReplyContent extends StatelessWidget {
); );
} else { } else {
replyBody = Text( replyBody = Text(
displayEvent.getLocalizedBody( displayEvent.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, withSenderNamePrefix: false,
hideReply: true, hideReply: true,
@ -83,18 +83,25 @@ class ReplyContent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text( FutureBuilder<User?>(
displayEvent.sender.calcDisplayname() + ':', future: displayEvent.fetchSenderUser(),
maxLines: 1, builder: (context, snapshot) {
overflow: TextOverflow.ellipsis, return Text(
style: TextStyle( (snapshot.data?.calcDisplayname() ??
fontWeight: FontWeight.bold, displayEvent.senderFromMemoryOrFallback
color: ownMessage .calcDisplayname()) +
? Theme.of(context).colorScheme.onPrimary ':',
: Theme.of(context).colorScheme.onBackground, maxLines: 1,
fontSize: fontSize, overflow: TextOverflow.ellipsis,
), style: TextStyle(
), fontWeight: FontWeight.bold,
color: ownMessage
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onBackground,
fontSize: fontSize,
),
);
}),
replyBody, replyBody,
], ],
), ),

View File

@ -39,16 +39,24 @@ class StateMessage extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( FutureBuilder<String>(
event.getLocalizedBody(MatrixLocals(L10n.of(context)!)), future: event
textAlign: TextAlign.center, .calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
style: TextStyle( builder: (context, snapshot) {
fontSize: 14 * AppConfig.fontSizeFactor, return Text(
color: Theme.of(context).textTheme.bodyText2!.color, snapshot.data ??
decoration: event.calcLocalizedBodyFallback(
event.redacted ? TextDecoration.lineThrough : null, MatrixLocals(L10n.of(context)!)),
), textAlign: TextAlign.center,
), style: TextStyle(
fontSize: 14 * AppConfig.fontSizeFactor,
color: Theme.of(context).textTheme.bodyText2!.color,
decoration: event.redacted
? TextDecoration.lineThrough
: null,
),
);
}),
if (counter != 0) if (counter != 0)
Text( Text(
L10n.of(context)!.moreEvents(counter), L10n.of(context)!.moreEvents(counter),

View File

@ -26,7 +26,7 @@ class PinnedEvents extends StatelessWidget {
actions: events actions: events
.map((event) => SheetAction( .map((event) => SheetAction(
key: event?.eventId ?? '', key: event?.eventId ?? '',
label: event?.getLocalizedBody( label: event?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true, withSenderNamePrefix: true,
hideReply: true, hideReply: true,
@ -90,32 +90,41 @@ class PinnedEvents extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: LinkText( child: FutureBuilder<String>(
text: event.getLocalizedBody( future: event.calcLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true, withSenderNamePrefix: true,
hideReply: true, hideReply: true,
), ),
maxLines: 2, builder: (context, snapshot) {
textStyle: TextStyle( return LinkText(
overflow: TextOverflow.ellipsis, text: snapshot.data ??
fontSize: fontSize, event.calcLocalizedBodyFallback(
decoration: event.redacted MatrixLocals(L10n.of(context)!),
? TextDecoration.lineThrough withSenderNamePrefix: true,
: null, hideReply: true,
), ),
linkStyle: TextStyle( maxLines: 2,
color: Theme.of(context) textStyle: TextStyle(
.textTheme overflow: TextOverflow.ellipsis,
.bodyText1 fontSize: fontSize,
?.color decoration: event.redacted
?.withAlpha(150), ? TextDecoration.lineThrough
fontSize: fontSize, : null,
decoration: TextDecoration.underline, ),
), linkStyle: TextStyle(
onLinkTap: (url) => color: Theme.of(context)
UrlLauncher(context, url).launchUrl(), .textTheme
), .bodyText1
?.color
?.withAlpha(150),
fontSize: fontSize,
decoration: TextDecoration.underline,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
);
}),
), ),
), ),
], ],

View File

@ -50,6 +50,7 @@ class _EditContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final event = this.event;
if (event == null) { if (event == null) {
return Container(); return Container();
} }
@ -60,19 +61,27 @@ class _EditContent extends StatelessWidget {
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
), ),
Container(width: 15.0), Container(width: 15.0),
Text( FutureBuilder<String>(
event?.getLocalizedBody( future: event.calcLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, withSenderNamePrefix: false,
hideReply: true, hideReply: true,
) ?? ),
'', builder: (context, snapshot) {
overflow: TextOverflow.ellipsis, return Text(
maxLines: 1, snapshot.data ??
style: TextStyle( event.calcLocalizedBodyFallback(
color: Theme.of(context).textTheme.bodyText2!.color, MatrixLocals(L10n.of(context)!),
), withSenderNamePrefix: false,
), hideReply: true,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Theme.of(context).textTheme.bodyText2!.color,
),
);
}),
], ],
); );
} }

View File

@ -94,15 +94,18 @@ class ChatEncryptionSettingsView extends StatelessWidget {
child: ListTile( child: ListTile(
leading: Avatar( leading: Avatar(
mxContent: room mxContent: room
.getUserByMXIDSync(deviceKeys[i].userId) .unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.avatarUrl, .avatarUrl,
name: room name: room
.getUserByMXIDSync(deviceKeys[i].userId) .unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.calcDisplayname(), .calcDisplayname(),
), ),
title: Text( title: Text(
room room
.getUserByMXIDSync(deviceKeys[i].userId) .unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.calcDisplayname(), .calcDisplayname(),
), ),
subtitle: Text( subtitle: Text(

View File

@ -262,32 +262,47 @@ class ChatListItem extends StatelessWidget {
), ),
softWrap: false, softWrap: false,
) )
: Text( : FutureBuilder<String>(
room.membership == Membership.invite future: room.lastEvent?.calcLocalizedBody(
? L10n.of(context)!.youAreInvitedToThisChat MatrixLocals(L10n.of(context)!),
: room.lastEvent?.getLocalizedBody( hideReply: true,
MatrixLocals(L10n.of(context)!), hideEdit: true,
hideReply: true, plaintextBody: true,
hideEdit: true, removeMarkdown: true,
plaintextBody: true, withSenderNamePrefix: !room.isDirectChat ||
removeMarkdown: true, room.directChatMatrixID !=
withSenderNamePrefix: !room.isDirectChat || room.lastEvent?.senderId,
room.directChatMatrixID != ) ??
room.lastEvent?.senderId, Future.value(L10n.of(context)!.emptyChat),
) ?? builder: (context, snapshot) {
L10n.of(context)!.emptyChat, return Text(
softWrap: false, room.membership == Membership.invite
maxLines: 1, ? L10n.of(context)!.youAreInvitedToThisChat
overflow: TextOverflow.ellipsis, : snapshot.data ??
style: TextStyle( room.lastEvent?.calcLocalizedBodyFallback(
color: unread MatrixLocals(L10n.of(context)!),
? Theme.of(context).colorScheme.secondary hideReply: true,
: Theme.of(context).textTheme.bodyText2!.color, hideEdit: true,
decoration: room.lastEvent?.redacted == true plaintextBody: true,
? TextDecoration.lineThrough removeMarkdown: true,
: null, withSenderNamePrefix: !room.isDirectChat ||
), room.directChatMatrixID !=
), room.lastEvent?.senderId,
) ??
L10n.of(context)!.emptyChat,
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unread
? Theme.of(context).colorScheme.secondary
: Theme.of(context).textTheme.bodyText2!.color,
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
);
}),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
AnimatedContainer( AnimatedContainer(

View File

@ -48,7 +48,7 @@ class ClientChooserButton extends StatelessWidget {
(client) => PopupMenuItem( (client) => PopupMenuItem(
value: client, value: client,
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: client!.ownProfile, future: client!.fetchOwnProfile(),
builder: (context, snapshot) => Row( builder: (context, snapshot) => Row(
children: [ children: [
Avatar( Avatar(
@ -90,7 +90,7 @@ class ClientChooserButton extends StatelessWidget {
matrix.accountBundles.forEach((key, value) => clientCount += value.length); matrix.accountBundles.forEach((key, value) => clientCount += value.length);
return Center( return Center(
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: matrix.client.ownProfile, future: matrix.client.fetchOwnProfile(),
builder: (context, snapshot) => Stack( builder: (context, snapshot) => Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [

View File

@ -38,7 +38,7 @@ class InvitationSelectionController extends State<InvitationSelection> {
final participantsIds = participants.map((p) => p.stateKey).toList(); final participantsIds = participants.map((p) => p.stateKey).toList();
final contacts = client.rooms final contacts = client.rooms
.where((r) => r.isDirectChat) .where((r) => r.isDirectChat)
.map((r) => r.getUserByMXIDSync(r.directChatMatrixID!)) .map((r) => r.unsafeGetUserFromMemoryOrFallback(r.directChatMatrixID!))
.toList() .toList()
..removeWhere((u) => participantsIds.contains(u.stateKey)); ..removeWhere((u) => participantsIds.contains(u.stateKey));
contacts.sort( contacts.sort(

View File

@ -111,7 +111,7 @@ class _KeyVerificationPageState extends State<KeyVerificationDialog> {
if (directChatId != null) { if (directChatId != null) {
user = widget.request.client user = widget.request.client
.getRoomById(directChatId)! .getRoomById(directChatId)!
.getUserByMXIDSync(widget.request.userId); .unsafeGetUserFromMemoryOrFallback(widget.request.userId);
} }
final displayName = final displayName =
user?.calcDisplayname() ?? widget.request.userId.localpart!; user?.calcDisplayname() ?? widget.request.userId.localpart!;

View File

@ -46,13 +46,11 @@ class SearchView extends StatelessWidget {
}).then((QueryPublicRoomsResponse res) { }).then((QueryPublicRoomsResponse res) {
final genericSearchTerm = controller.genericSearchTerm; final genericSearchTerm = controller.genericSearchTerm;
if (genericSearchTerm != null && if (genericSearchTerm != null &&
!res.chunk.any((room) => !res.chunk.any(
(room.aliases?.contains(controller.genericSearchTerm) ?? false) || (room) => room.canonicalAlias == controller.genericSearchTerm)) {
room.canonicalAlias == controller.genericSearchTerm)) {
// we have to tack on the original alias // we have to tack on the original alias
res.chunk.add( res.chunk.add(
PublicRoomsChunk( PublicRoomsChunk(
aliases: [genericSearchTerm],
name: genericSearchTerm, name: genericSearchTerm,
numJoinedMembers: 0, numJoinedMembers: 0,
roomId: '!unknown', roomId: '!unknown',

View File

@ -370,7 +370,7 @@ class StoryPageController extends State<StoryPage> {
.client .client
.getRoomById(roomId) .getRoomById(roomId)
?.getState(EventTypes.RoomCreate) ?.getState(EventTypes.RoomCreate)
?.sender ?.senderFromMemoryOrFallback
.avatarUrl; .avatarUrl;
String get title => String get title =>
@ -378,7 +378,7 @@ class StoryPageController extends State<StoryPage> {
.client .client
.getRoomById(roomId) .getRoomById(roomId)
?.getState(EventTypes.RoomCreate) ?.getState(EventTypes.RoomCreate)
?.sender ?.senderFromMemoryOrFallback
.calcDisplayname() ?? .calcDisplayname() ??
'Story not found'; 'Story not found';
@ -485,7 +485,8 @@ class StoryPageController extends State<StoryPage> {
case PopupStoryAction.message: case PopupStoryAction.message:
final roomIdResult = await showFutureLoadingDialog( final roomIdResult = await showFutureLoadingDialog(
context: context, context: context,
future: () => currentEvent!.sender.startDirectChat(), future: () =>
currentEvent!.senderFromMemoryOrFallback.startDirectChat(),
); );
if (roomIdResult.error != null) return; if (roomIdResult.error != null) return;
VRouter.of(context).toSegments(['rooms', roomIdResult.result!]); VRouter.of(context).toSegments(['rooms', roomIdResult.result!]);

View File

@ -8,10 +8,10 @@ import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/utils/custom_image_resizer.dart'; import 'package:fluffychat/utils/custom_image_resizer.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/flutter_hive_collections_database.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'famedlysdk_store.dart'; import 'famedlysdk_store.dart';
import 'matrix_sdk_extensions.dart/fluffybox_database.dart'; import 'matrix_sdk_extensions.dart/fluffybox_database.dart';
import 'matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart';
abstract class ClientManager { abstract class ClientManager {
static const String clientNamespace = 'im.fluffychat.store.clients'; static const String clientNamespace = 'im.fluffychat.store.clients';
@ -95,8 +95,8 @@ abstract class ClientManager {
// To check which story room we can post in // To check which story room we can post in
EventTypes.RoomPowerLevels, EventTypes.RoomPowerLevels,
}, },
databaseBuilder: FlutterFluffyBoxDatabase.databaseBuilder, databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder,
legacyDatabaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder, legacyDatabaseBuilder: FlutterFluffyBoxDatabase.databaseBuilder,
supportedLoginTypes: { supportedLoginTypes: {
AuthenticationTypes.password, AuthenticationTypes.password,
if (PlatformInfos.isMobile || if (PlatformInfos.isMobile ||

View File

@ -12,7 +12,8 @@ extension ClientStoriesExtension on Client {
List<User> get contacts => rooms List<User> get contacts => rooms
.where((room) => room.isDirectChat) .where((room) => room.isDirectChat)
.map((room) => room.getUserByMXIDSync(room.directChatMatrixID!)) .map((room) =>
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!))
.toList(); .toList();
List<Room> get storiesRooms => rooms List<Room> get storiesRooms => rooms

View File

@ -14,6 +14,7 @@ import 'package:path_provider/path_provider.dart';
import '../client_manager.dart'; import '../client_manager.dart';
import '../famedlysdk_store.dart'; import '../famedlysdk_store.dart';
// ignore: deprecated_member_use
class FlutterFluffyBoxDatabase extends FluffyBoxDatabase { class FlutterFluffyBoxDatabase extends FluffyBoxDatabase {
FlutterFluffyBoxDatabase( FlutterFluffyBoxDatabase(
String name, String name,
@ -27,6 +28,7 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase {
static const String _cipherStorageKey = 'database_encryption_key'; static const String _cipherStorageKey = 'database_encryption_key';
// ignore: deprecated_member_use
static Future<FluffyBoxDatabase> databaseBuilder(Client client) async { static Future<FluffyBoxDatabase> databaseBuilder(Client client) async {
Logs().d('Open FluffyBox...'); Logs().d('Open FluffyBox...');
fluffybox.HiveAesCipher? hiverCipher; fluffybox.HiveAesCipher? hiverCipher;
@ -59,6 +61,7 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase {
rethrow; rethrow;
} }
// ignore: deprecated_member_use
final db = FluffyBoxDatabase( final db = FluffyBoxDatabase(
'fluffybox_${client.clientName.replaceAll(' ', '_').toLowerCase()}', 'fluffybox_${client.clientName.replaceAll(' ', '_').toLowerCase()}',
await _findDatabasePath(client), await _findDatabasePath(client),

View File

@ -2,78 +2,108 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart' hide Key;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../platform_infos.dart'; class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
FlutterHiveCollectionsDatabase(
class FlutterMatrixHiveStore extends FamedlySdkHiveDatabase { String name,
FlutterMatrixHiveStore(String name, {HiveCipher? encryptionCipher}) String path, {
: super( HiveCipher? key,
}) : super(
name, name,
encryptionCipher: encryptionCipher, path,
key: key,
); );
static bool _hiveInitialized = false; static const String _cipherStorageKey = 'database_encryption_key';
static const String _hiveCipherStorageKey = 'hive_encryption_key';
static Future<FamedlySdkHiveDatabase> hiveDatabaseBuilder( static Future<FlutterHiveCollectionsDatabase> databaseBuilder(
Client client) async { Client client) async {
if (!kIsWeb && !_hiveInitialized) { Logs().d('Open Hive...');
_hiveInitialized = true; HiveAesCipher? hiverCipher;
}
HiveCipher? hiverCipher;
try { try {
// Workaround for secure storage is calling Platform.operatingSystem on web // Workaround for secure storage is calling Platform.operatingSystem on web
if (kIsWeb || Platform.isLinux) throw MissingPluginException(); if (kIsWeb) throw MissingPluginException();
const secureStorage = FlutterSecureStorage(); const secureStorage = FlutterSecureStorage();
final containsEncryptionKey = final containsEncryptionKey =
await secureStorage.containsKey(key: _hiveCipherStorageKey); await secureStorage.containsKey(key: _cipherStorageKey);
if (!containsEncryptionKey) { if (!containsEncryptionKey) {
// do not try to create a buggy secure storage for new Linux users
if (Platform.isLinux) throw MissingPluginException();
final key = Hive.generateSecureKey(); final key = Hive.generateSecureKey();
await secureStorage.write( await secureStorage.write(
key: _hiveCipherStorageKey, key: _cipherStorageKey,
value: base64UrlEncode(key), value: base64UrlEncode(key),
); );
} }
// workaround for if we just wrote to the key and it still doesn't exist // workaround for if we just wrote to the key and it still doesn't exist
final rawEncryptionKey = final rawEncryptionKey = await secureStorage.read(key: _cipherStorageKey);
await secureStorage.read(key: _hiveCipherStorageKey);
if (rawEncryptionKey == null) throw MissingPluginException(); if (rawEncryptionKey == null) throw MissingPluginException();
final encryptionKey = base64Url.decode(rawEncryptionKey); hiverCipher = HiveAesCipher(base64Url.decode(rawEncryptionKey));
hiverCipher = HiveAesCipher(encryptionKey);
} on MissingPluginException catch (_) { } on MissingPluginException catch (_) {
Logs().i('Hive encryption is not supported on this platform'); Logs().i('Hive encryption is not supported on this platform');
} catch (_) {
const FlutterSecureStorage().delete(key: _cipherStorageKey);
rethrow;
} }
final db = FlutterMatrixHiveStore(
client.clientName, final db = FlutterHiveCollectionsDatabase(
encryptionCipher: hiverCipher, 'hive_collections_${client.clientName.replaceAll(' ', '_').toLowerCase()}',
await _findDatabasePath(client),
key: hiverCipher,
); );
try { try {
await db.open(); await db.open();
} catch (e, s) { } catch (_) {
Logs().e('Unable to open Hive. Delete and try again...', e, s); Logs().w('Unable to open Hive. Delete database and storage key...');
const FlutterSecureStorage().delete(key: _cipherStorageKey);
await db.clear(); await db.clear();
await db.open(); rethrow;
} }
Logs().d('Hive is ready');
return db; return db;
} }
static Future<String> _findDatabasePath(Client client) async {
String path = client.clientName;
if (!kIsWeb) {
Directory directory;
try {
if (Platform.isLinux) {
directory = await getApplicationSupportDirectory();
} else {
directory = await getApplicationDocumentsDirectory();
}
} catch (_) {
try {
directory = await getLibraryDirectory();
} catch (_) {
directory = Directory.current;
}
}
// do not destroy your stable FluffyChat in debug mode
if (kDebugMode) {
directory = Directory(directory.uri.resolve('debug').toFilePath());
directory.create(recursive: true);
}
path = directory.path;
}
return path;
}
@override @override
int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0; int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0;
@override @override
bool get supportsFileStoring => (PlatformInfos.isIOS || bool get supportsFileStoring => !kIsWeb;
PlatformInfos.isAndroid ||
PlatformInfos.isDesktop);
Future<String> _getFileStoreDirectory() async { Future<String> _getFileStoreDirectory() async {
try { try {

View File

@ -62,7 +62,7 @@ Future<void> pushHelper(
final matrixLocals = MatrixLocals(l10n); final matrixLocals = MatrixLocals(l10n);
// Calculate the body // Calculate the body
final body = event.getLocalizedBody( final body = await event.calcLocalizedBody(
matrixLocals, matrixLocals,
plaintextBody: true, plaintextBody: true,
withSenderNamePrefix: !event.room.isDirectChat, withSenderNamePrefix: !event.room.isDirectChat,

View File

@ -31,7 +31,7 @@ extension LocalNotificationsExtension on MatrixState {
final event = Event.fromJson(eventUpdate.content, room); final event = Event.fromJson(eventUpdate.content, room);
final title = final title =
room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)!)); room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)!));
final body = event.getLocalizedBody( final body = await event.calcLocalizedBody(
MatrixLocals(L10n.of(widget.context)!), MatrixLocals(L10n.of(widget.context)!),
withSenderNamePrefix: withSenderNamePrefix:
!room.isDirectChat || room.lastEvent?.senderId == client.userID, !room.isDirectChat || room.lastEvent?.senderId == client.userID,
@ -40,8 +40,11 @@ extension LocalNotificationsExtension on MatrixState {
hideEdit: true, hideEdit: true,
removeMarkdown: true, removeMarkdown: true,
); );
final icon = event.sender.avatarUrl?.getThumbnail(client, final icon = event.senderFromMemoryOrFallback.avatarUrl?.getThumbnail(
width: 64, height: 64, method: ThumbnailMethod.crop) ?? client,
width: 64,
height: 64,
method: ThumbnailMethod.crop) ??
room.avatar?.getThumbnail(client, room.avatar?.getThumbnail(client,
width: 64, height: 64, method: ThumbnailMethod.crop); width: 64, height: 64, method: ThumbnailMethod.crop);
if (kIsWeb) { if (kIsWeb) {

View File

@ -41,9 +41,7 @@ class PublicRoomBottomSheet extends StatelessWidget {
} }
} }
bool _testRoom(PublicRoomsChunk r) => bool _testRoom(PublicRoomsChunk r) => r.canonicalAlias == roomAlias;
r.canonicalAlias == roomAlias ||
(r.aliases?.contains(roomAlias) ?? false);
Future<PublicRoomsChunk> _search(BuildContext context) async { Future<PublicRoomsChunk> _search(BuildContext context) async {
final chunk = this.chunk; final chunk = this.chunk;

View File

@ -395,6 +395,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.1" version: "5.0.1"
enhanced_enum:
dependency: transitive
description:
name: enhanced_enum
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -788,7 +795,7 @@ packages:
name: hive name: hive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.2.1"
hive_flutter: hive_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1012,21 +1019,21 @@ packages:
name: matrix name: matrix
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.6" version: "0.9.12"
matrix_api_lite: matrix_api_lite:
dependency: transitive dependency: transitive
description: description:
name: matrix_api_lite name: matrix_api_lite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.3" version: "1.1.1"
matrix_homeserver_recommendations: matrix_homeserver_recommendations:
dependency: "direct main" dependency: "direct main"
description: description:
name: matrix_homeserver_recommendations name: matrix_homeserver_recommendations
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1"
matrix_link_text: matrix_link_text:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -57,7 +57,7 @@ dependencies:
keyboard_shortcuts: ^0.1.4 keyboard_shortcuts: ^0.1.4
localstorage: ^4.0.0+1 localstorage: ^4.0.0+1
lottie: ^1.2.2 lottie: ^1.2.2
matrix: ^0.9.4 matrix: ^0.9.12
matrix_homeserver_recommendations: ^0.2.0 matrix_homeserver_recommendations: ^0.2.0
matrix_link_text: ^1.0.2 matrix_link_text: ^1.0.2
native_imaging: native_imaging:

View File

@ -2,7 +2,7 @@ import 'package:matrix/encryption/utils/key_verification.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix_api_lite/fake_matrix_api.dart'; import 'package:matrix_api_lite/fake_matrix_api.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/flutter_hive_collections_database.dart';
Future<Client> prepareTestClient({ Future<Client> prepareTestClient({
bool loggedIn = false, bool loggedIn = false,
@ -20,7 +20,7 @@ Future<Client> prepareTestClient({
importantStateEvents: <String>{ importantStateEvents: <String>{
'im.ponies.room_emotes', // we want emotes to work properly 'im.ponies.room_emotes', // we want emotes to work properly
}, },
databaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder, databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder,
supportedLoginTypes: { supportedLoginTypes: {
AuthenticationTypes.password, AuthenticationTypes.password,
AuthenticationTypes.sso AuthenticationTypes.sso