fluffychat merge
This commit is contained in:
commit
540567bfe7
18 changed files with 1345 additions and 279 deletions
|
|
@ -1,4 +1,4 @@
|
|||
FluffyChat is an open, nonprofit and cute matrix messenger app for Ubuntu Touch, Android and iOS.
|
||||
FluffyChat is an open, nonprofit and cute Matrix messenger app for Ubuntu Touch, Android and iOS.
|
||||
|
||||
Open
|
||||
Opensource and open development where everyone can join.
|
||||
|
|
@ -9,7 +9,7 @@ FluffyChat is donation funded.
|
|||
Cute ♥
|
||||
Cute design and many theme settings including a dark mode.
|
||||
|
||||
One-to-one and groupchats
|
||||
One-to-one and group chats
|
||||
Unlimited groups and direct chats.
|
||||
|
||||
Easy
|
||||
|
|
@ -22,11 +22,11 @@ Decentralized
|
|||
There is no "FluffyChat server" you are forced to use. Use the server you find trustworthy or host your own.
|
||||
|
||||
Compatible
|
||||
Compatible with Element, Fractal, Nheko and all matrix messengers.
|
||||
Compatible with Element, Fractal, Nheko and all Matrix messengers.
|
||||
|
||||
|
||||
FluffyChat comes with a dream
|
||||
|
||||
Imagine a world where everyone can choose the messenger they like and is still able to chat with all of their friends.
|
||||
A world where there are no companies spying on you when you send selfies to friends and lovers.
|
||||
A world where there are no companies spying on you when you send selfies to friends and your loved.
|
||||
And a world where apps are made for fluffyness and not for profit. ♥
|
||||
|
|
|
|||
32
android/fastlane/metadata/android/ru/full_description.txt
Normal file
32
android/fastlane/metadata/android/ru/full_description.txt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
FluffyChat это свободный, некоммерческий и милый чат Matrix для Ubuntu Touch, Android и iOS.
|
||||
|
||||
Открыть
|
||||
Открытый исходный код и открытая разработка, где присоединиться может каждый.
|
||||
|
||||
Некоммерческий
|
||||
FluffyChat финансируется пожертвованиями.
|
||||
|
||||
Милый ♥
|
||||
Симпатичный дизайн и много настроек темы, включая тёмный режим.
|
||||
|
||||
Личные и групповые чаты
|
||||
Неограниченные группы и прямые чаты один на один.
|
||||
|
||||
Легкий
|
||||
FluffyChat сделан максимально простым в использовании.
|
||||
|
||||
Бесплатный
|
||||
Бесплатное использование для всех без рекламы.
|
||||
|
||||
Децентрализованный
|
||||
Нет единого «FluffyChat сервера» который вас принуждают использовать. Используйте сервер, который вы находите надёжным или создайте свой собственный.
|
||||
|
||||
Совместимый
|
||||
Совместим с Element, Fractal, Nheko и всеми мессенджерами Matrix.
|
||||
|
||||
|
||||
FluffyChat стремится к мечте
|
||||
|
||||
Представьте себе мир где каждый может выбрать чат который ему нравится и все еще иметь возможность общаться со всеми своими друзьями.
|
||||
Мир, где нет компаний, шпионящих за тобой, когда ты посылаешь селфи друзьям и любимым.
|
||||
И мир, где приложения созданы для пушистости, а не для прибыли. ♥
|
||||
|
|
@ -0,0 +1 @@
|
|||
Общайтесь с друзьями с FluffyChat
|
||||
|
|
@ -3811,6 +3811,16 @@
|
|||
"level": {}
|
||||
}
|
||||
},
|
||||
"searchIn": "Search in {chat}...",
|
||||
"@searchIn": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"chat": {}
|
||||
}
|
||||
},
|
||||
"searchMore": "Search more...",
|
||||
"gallery": "Gallery",
|
||||
"files": "Files",
|
||||
"databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}",
|
||||
"@databaseBuildErrorBody": {
|
||||
"type": "text",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
|||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_members/chat_members.dart';
|
||||
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
||||
import 'package:fluffychat/pages/chat_search/chat_search_page.dart';
|
||||
import 'package:fluffychat/pages/device_settings/device_settings.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
||||
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
||||
|
|
@ -262,6 +263,7 @@ abstract class AppRoutes {
|
|||
state,
|
||||
ChatPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
eventId: state.uri.queryParameters['event'],
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -511,10 +513,22 @@ abstract class AppRoutes {
|
|||
ChatPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
shareText: state.uri.queryParameters['body'],
|
||||
eventId: state.uri.queryParameters['event'],
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'search',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ChatSearchPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
// #Pangea
|
||||
// GoRoute(
|
||||
// path: 'encryption',
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import 'package:fluffychat/widgets/app_lock.dart';
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
|
|
@ -57,10 +56,12 @@ import 'send_location_dialog.dart';
|
|||
class ChatPage extends StatelessWidget {
|
||||
final String roomId;
|
||||
final String? shareText;
|
||||
final String? eventId;
|
||||
|
||||
const ChatPage({
|
||||
super.key,
|
||||
required this.roomId,
|
||||
this.eventId,
|
||||
this.shareText,
|
||||
});
|
||||
|
||||
|
|
@ -87,6 +88,7 @@ class ChatPage extends StatelessWidget {
|
|||
key: Key('chat_page_$roomId'),
|
||||
room: room,
|
||||
shareText: shareText,
|
||||
eventId: eventId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -94,11 +96,13 @@ class ChatPage extends StatelessWidget {
|
|||
class ChatPageWithRoom extends StatefulWidget {
|
||||
final Room room;
|
||||
final String? shareText;
|
||||
final String? eventId;
|
||||
|
||||
const ChatPageWithRoom({
|
||||
super.key,
|
||||
required this.room,
|
||||
this.shareText,
|
||||
this.eventId,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -298,12 +302,14 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
void initState() {
|
||||
scrollController.addListener(_updateScrollController);
|
||||
inputFocus.addListener(_inputFocusListener);
|
||||
|
||||
_loadDraft();
|
||||
super.initState();
|
||||
_displayChatDetailsColumn = ValueNotifier(
|
||||
Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
|
||||
false,
|
||||
);
|
||||
|
||||
sendingClient = Matrix.of(context).client;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// #Pangea
|
||||
|
|
@ -354,7 +360,8 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
void _tryLoadTimeline() async {
|
||||
loadTimelineFuture = _getTimeline();
|
||||
readMarkerEventId = widget.eventId;
|
||||
loadTimelineFuture = _getTimeline(eventContextId: readMarkerEventId);
|
||||
try {
|
||||
await loadTimelineFuture;
|
||||
final fullyRead = room.fullyRead;
|
||||
|
|
@ -458,18 +465,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
timeline!.requestKeys(onlineKeyBackupOnly: false);
|
||||
if (room.markedUnread) room.markUnread(false);
|
||||
|
||||
// when the scroll controller is attached we want to scroll to an event id, if specified
|
||||
// and update the scroll controller...which will trigger a request history, if the
|
||||
// "load more" button is visible on the screen
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||
if (mounted) {
|
||||
final event = GoRouterState.of(context).uri.queryParameters['event'];
|
||||
if (event != null) {
|
||||
scrollToEventId(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -241,6 +241,9 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
if (audioPlayer == null) return;
|
||||
switch (audioPlayer.speed) {
|
||||
case 1.0:
|
||||
await audioPlayer.setSpeed(1.25);
|
||||
break;
|
||||
case 1.25:
|
||||
await audioPlayer.setSpeed(1.5);
|
||||
break;
|
||||
case 1.5:
|
||||
|
|
@ -295,7 +298,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
final statusText = this.statusText ??= _durationString ?? '00:00';
|
||||
final audioPlayer = this.audioPlayer;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
|
@ -330,80 +333,70 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => audioPlayer?.seek(
|
||||
Duration(
|
||||
milliseconds:
|
||||
(maxPosition / AudioPlayerWidget.wavesCount)
|
||||
.round() *
|
||||
i,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
height: 32,
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: currentPosition > i ? 1 : 0.5,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
),
|
||||
height: 32 * (waveform[i] / 1024),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
|
||||
GestureDetector(
|
||||
onTapDown: (_) => audioPlayer?.seek(
|
||||
Duration(
|
||||
milliseconds:
|
||||
(maxPosition / AudioPlayerWidget.wavesCount).round() *
|
||||
i,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
alignment: Alignment.centerRight,
|
||||
width: 42,
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
child: InkWell(
|
||||
splashColor: widget.color.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
onTap: audioPlayer == null ? null : _toggleSpeed,
|
||||
child: Icon(Icons.mic_none_outlined, color: widget.color),
|
||||
),
|
||||
),
|
||||
if (audioPlayer != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Text(
|
||||
'${audioPlayer.speed.toString()}x',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 9.0,
|
||||
color: widget.color,
|
||||
child: Container(
|
||||
height: 32,
|
||||
color: widget.color.withAlpha(0),
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: currentPosition > i ? 1 : 0.5,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
width: 2,
|
||||
height: 32 * (waveform[i] / 1024),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 36,
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Badge(
|
||||
isLabelVisible: audioPlayer != null,
|
||||
label: audioPlayer == null
|
||||
? null
|
||||
: Text(
|
||||
'${audioPlayer.speed.toString()}x',
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
textColor: Theme.of(context).colorScheme.onSecondary,
|
||||
child: InkWell(
|
||||
splashColor: widget.color.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
onTap: audioPlayer == null ? null : _toggleSpeed,
|
||||
child: Icon(
|
||||
Icons.mic_none_outlined,
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class ImageBubble extends StatelessWidget {
|
|||
this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius);
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
side: BorderSide(
|
||||
|
|
|
|||
|
|
@ -188,10 +188,7 @@ class MessageContent extends StatelessWidget {
|
|||
}
|
||||
return MessageDownloadContent(event, textColor);
|
||||
case MessageTypes.Video:
|
||||
if (PlatformInfos.isMobile || PlatformInfos.isWeb) {
|
||||
return EventVideoPlayer(event);
|
||||
}
|
||||
return MessageDownloadContent(event, textColor);
|
||||
return EventVideoPlayer(event);
|
||||
case MessageTypes.File:
|
||||
return MessageDownloadContent(event, textColor);
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class MessageReactions extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
event.room.sendReaction(event.eventId, r.key!);
|
||||
event.room.sendReaction(event.eventId, r.key);
|
||||
}
|
||||
},
|
||||
onLongPress: () async => await _AdaptableReactorsDialog(
|
||||
|
|
@ -92,7 +92,7 @@ class MessageReactions extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _Reaction extends StatelessWidget {
|
||||
final String? reactionKey;
|
||||
final String reactionKey;
|
||||
final int count;
|
||||
final bool? reacted;
|
||||
final void Function()? onTap;
|
||||
|
|
@ -112,16 +112,16 @@ class _Reaction extends StatelessWidget {
|
|||
? Colors.white
|
||||
: Colors.black;
|
||||
final color = Theme.of(context).colorScheme.background;
|
||||
final fontSize = DefaultTextStyle.of(context).style.fontSize;
|
||||
Widget content;
|
||||
if (reactionKey!.startsWith('mxc://')) {
|
||||
if (reactionKey.startsWith('mxc://')) {
|
||||
content = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
MxcImage(
|
||||
uri: Uri.parse(reactionKey!),
|
||||
width: 9999,
|
||||
height: fontSize,
|
||||
uri: Uri.parse(reactionKey),
|
||||
width: 20,
|
||||
height: 20,
|
||||
animated: false,
|
||||
),
|
||||
if (count > 1) ...[
|
||||
const SizedBox(width: 4),
|
||||
|
|
@ -136,7 +136,7 @@ class _Reaction extends StatelessWidget {
|
|||
],
|
||||
);
|
||||
} else {
|
||||
var renderKey = Characters(reactionKey!);
|
||||
var renderKey = Characters(reactionKey);
|
||||
if (renderKey.length > 10) {
|
||||
renderKey = renderKey.getRange(0, 9) + Characters('…');
|
||||
}
|
||||
|
|
@ -171,13 +171,13 @@ class _Reaction extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _ReactionEntry {
|
||||
String? key;
|
||||
String key;
|
||||
int count;
|
||||
bool reacted;
|
||||
List<User>? reactors;
|
||||
|
||||
_ReactionEntry({
|
||||
this.key,
|
||||
required this.key,
|
||||
required this.count,
|
||||
required this.reacted,
|
||||
this.reactors,
|
||||
|
|
@ -222,7 +222,7 @@ class _AdaptableReactorsDialog extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
|
||||
final title = Center(child: Text(reactionEntry!.key!));
|
||||
final title = Center(child: Text(reactionEntry!.key));
|
||||
|
||||
return AlertDialog.adaptive(
|
||||
title: title,
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/blur_hash.dart';
|
||||
import '../../../utils/error_reporter.dart';
|
||||
|
||||
|
|
@ -31,6 +33,10 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
File? _tmpFile;
|
||||
|
||||
void _downloadAction() async {
|
||||
if (PlatformInfos.isDesktop) {
|
||||
widget.event.saveFile(context);
|
||||
return;
|
||||
}
|
||||
setState(() => _isDownloading = true);
|
||||
try {
|
||||
final videoFile = await widget.event.downloadAndDecryptAttachment();
|
||||
|
|
@ -98,6 +104,7 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
final chewieManager = _chewieManager;
|
||||
return Material(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: chewieManager != null
|
||||
|
|
@ -114,9 +121,10 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
else
|
||||
BlurHash(blurhash: blurHash, width: 300, height: 300),
|
||||
Center(
|
||||
child: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.background,
|
||||
),
|
||||
icon: _isDownloading
|
||||
? const SizedBox(
|
||||
|
|
@ -126,14 +134,12 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.download_outlined),
|
||||
label: Text(
|
||||
_isDownloading
|
||||
? L10n.of(context)!.loadingPleaseWait
|
||||
: L10n.of(context)!.videoWithSize(
|
||||
widget.event.sizeString ?? '?MB',
|
||||
),
|
||||
),
|
||||
: const Icon(Icons.play_circle_outlined),
|
||||
tooltip: _isDownloading
|
||||
? L10n.of(context)!.loadingPleaseWait
|
||||
: L10n.of(context)!.videoWithSize(
|
||||
widget.event.sizeString ?? '?MB',
|
||||
),
|
||||
onPressed: _isDownloading ? null : _downloadAction,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
176
lib/pages/chat_search/chat_search_files_tab.dart
Normal file
176
lib/pages/chat_search/chat_search_files_tab.dart
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
||||
class ChatSearchFilesTab extends StatelessWidget {
|
||||
final Room room;
|
||||
final Stream<(List<Event>, String?)>? searchStream;
|
||||
final void Function({
|
||||
String? prevBatch,
|
||||
List<Event>? previousSearchResult,
|
||||
}) startSearch;
|
||||
|
||||
const ChatSearchFilesTab({
|
||||
required this.room,
|
||||
required this.startSearch,
|
||||
required this.searchStream,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: searchStream,
|
||||
builder: (context, snapshot) {
|
||||
final events = snapshot.data?.$1;
|
||||
if (searchStream == null || events == null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
L10n.of(context)!.searchIn(
|
||||
room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (events.isEmpty) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.file_present_outlined, size: 64),
|
||||
const SizedBox(height: 8),
|
||||
Text(L10n.of(context)!.nothingFound),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return SelectionArea(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
itemCount: events.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == events.length) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final nextBatch = snapshot.data?.$2;
|
||||
if (nextBatch == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
onPressed: () => startSearch(
|
||||
prevBatch: nextBatch,
|
||||
previousSearchResult: events,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.arrow_downward_outlined,
|
||||
),
|
||||
label: Text(L10n.of(context)!.searchMore),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final event = events[i];
|
||||
final filename = event.content.tryGet<String>('filename') ??
|
||||
event.content.tryGet<String>('body') ??
|
||||
L10n.of(context)!.unknownEvent('File');
|
||||
final filetype = (filename.contains('.')
|
||||
? filename.split('.').last.toUpperCase()
|
||||
: event.content
|
||||
.tryGetMap<String, dynamic>('info')
|
||||
?.tryGet<String>('mimetype')
|
||||
?.toUpperCase() ??
|
||||
'UNKNOWN');
|
||||
final sizeString = event.sizeString;
|
||||
final prevEvent = i > 0 ? events[i - 1] : null;
|
||||
final sameEnvironment = prevEvent == null
|
||||
? false
|
||||
: prevEvent.originServerTs
|
||||
.sameEnvironment(event.originServerTs);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!sameEnvironment) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
event.originServerTs.localizedTime(context),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
Material(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.file_present_outlined),
|
||||
title: Text(
|
||||
filename,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text('$sizeString | $filetype'),
|
||||
onTap: () => event.saveFile(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
169
lib/pages/chat_search/chat_search_images_tab.dart
Normal file
169
lib/pages/chat_search/chat_search_images_tab.dart
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
||||
class ChatSearchImagesTab extends StatelessWidget {
|
||||
final Room room;
|
||||
final Stream<(List<Event>, String?)>? searchStream;
|
||||
final void Function({
|
||||
String? prevBatch,
|
||||
List<Event>? previousSearchResult,
|
||||
}) startSearch;
|
||||
|
||||
const ChatSearchImagesTab({
|
||||
required this.room,
|
||||
required this.startSearch,
|
||||
required this.searchStream,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: searchStream,
|
||||
builder: (context, snapshot) {
|
||||
final events = snapshot.data?.$1;
|
||||
if (searchStream == null || events == null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
L10n.of(context)!.searchIn(
|
||||
room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (events.isEmpty) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.photo_outlined, size: 64),
|
||||
const SizedBox(height: 8),
|
||||
Text(L10n.of(context)!.nothingFound),
|
||||
],
|
||||
);
|
||||
}
|
||||
final eventsByMonth = <DateTime, List<Event>>{};
|
||||
for (final event in events) {
|
||||
final month = DateTime(
|
||||
event.originServerTs.year,
|
||||
event.originServerTs.month,
|
||||
);
|
||||
eventsByMonth[month] ??= [];
|
||||
eventsByMonth[month]!.add(event);
|
||||
}
|
||||
final eventsByMonthList = eventsByMonth.entries.toList();
|
||||
|
||||
const padding = 8.0;
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: eventsByMonth.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == eventsByMonth.length) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final nextBatch = snapshot.data?.$2;
|
||||
if (nextBatch == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
onPressed: () => startSearch(
|
||||
prevBatch: nextBatch,
|
||||
previousSearchResult: events,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.arrow_downward_outlined,
|
||||
),
|
||||
label: Text(L10n.of(context)!.searchMore),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final monthEvents = eventsByMonthList[i].value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
DateFormat.yMMMM(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
).format(eventsByMonthList[i].key),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GridView.count(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
mainAxisSpacing: padding,
|
||||
crossAxisSpacing: padding,
|
||||
padding: const EdgeInsets.all(padding),
|
||||
crossAxisCount: 3,
|
||||
children: monthEvents.map(
|
||||
(event) {
|
||||
if (event.messageType == MessageTypes.Video) {
|
||||
return EventVideoPlayer(event);
|
||||
}
|
||||
return ImageBubble(
|
||||
event,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
186
lib/pages/chat_search/chat_search_message_tab.dart
Normal file
186
lib/pages/chat_search/chat_search_message_tab.dart
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
class ChatSearchMessageTab extends StatelessWidget {
|
||||
final String searchQuery;
|
||||
final Room room;
|
||||
final Stream<(List<Event>, String?)>? searchStream;
|
||||
final void Function({
|
||||
String? prevBatch,
|
||||
List<Event>? previousSearchResult,
|
||||
}) startSearch;
|
||||
|
||||
const ChatSearchMessageTab({
|
||||
required this.searchQuery,
|
||||
required this.room,
|
||||
required this.searchStream,
|
||||
required this.startSearch,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
key: ValueKey(searchQuery),
|
||||
stream: searchStream,
|
||||
builder: (context, snapshot) {
|
||||
if (searchStream == null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.search_outlined, size: 64),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
L10n.of(context)!.searchIn(
|
||||
room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final events = snapshot.data?.$1 ?? [];
|
||||
|
||||
return SelectionArea(
|
||||
child: ListView.separated(
|
||||
itemCount: events.length + 1,
|
||||
separatorBuilder: (context, _) => Divider(
|
||||
color: Theme.of(context).dividerColor,
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == events.length) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final nextBatch = snapshot.data?.$2;
|
||||
if (nextBatch == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
onPressed: () => startSearch(
|
||||
prevBatch: nextBatch,
|
||||
previousSearchResult: events,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.arrow_downward_outlined,
|
||||
),
|
||||
label: Text(L10n.of(context)!.searchMore),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final event = events[i];
|
||||
final sender = event.senderFromMemoryOrFallback;
|
||||
final displayname = sender.calcDisplayname(
|
||||
i18n: MatrixLocals(L10n.of(context)!),
|
||||
);
|
||||
return _MessageSearchResultListTile(
|
||||
sender: sender,
|
||||
displayname: displayname,
|
||||
event: event,
|
||||
room: room,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MessageSearchResultListTile extends StatelessWidget {
|
||||
const _MessageSearchResultListTile({
|
||||
required this.sender,
|
||||
required this.displayname,
|
||||
required this.event,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
final User sender;
|
||||
final String displayname;
|
||||
final Event event;
|
||||
final Room room;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: sender.avatarUrl,
|
||||
name: displayname,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
displayname,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
' | ${event.originServerTs.localizedTimeShort(context)}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Linkify(
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
text: event
|
||||
.calcLocalizedBodyFallback(
|
||||
plaintextBody: true,
|
||||
removeMarkdown: true,
|
||||
MatrixLocals(
|
||||
L10n.of(context)!,
|
||||
),
|
||||
)
|
||||
.trim(),
|
||||
maxLines: 7,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
),
|
||||
onPressed: () => context.go(
|
||||
'/${Uri(
|
||||
pathSegments: ['rooms', room.id],
|
||||
queryParameters: {'event': event.eventId},
|
||||
)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
166
lib/pages/chat_search/chat_search_page.dart
Normal file
166
lib/pages/chat_search/chat_search_page.dart
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_search/chat_search_view.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ChatSearchPage extends StatefulWidget {
|
||||
final String roomId;
|
||||
const ChatSearchPage({required this.roomId, super.key});
|
||||
|
||||
@override
|
||||
ChatSearchController createState() => ChatSearchController();
|
||||
}
|
||||
|
||||
class ChatSearchController extends State<ChatSearchPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Room? get room => Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
late final TabController tabController;
|
||||
|
||||
Timeline? timeline;
|
||||
|
||||
Stream<(List<Event>, String?)>? searchStream;
|
||||
Stream<(List<Event>, String?)>? galleryStream;
|
||||
Stream<(List<Event>, String?)>? fileStream;
|
||||
|
||||
void restartSearch() {
|
||||
if (searchController.text.isEmpty) {
|
||||
setState(() {
|
||||
searchStream = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
searchStream = const Stream.empty();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
startMessageSearch();
|
||||
});
|
||||
}
|
||||
|
||||
void startMessageSearch({
|
||||
String? prevBatch,
|
||||
List<Event>? previousSearchResult,
|
||||
}) async {
|
||||
final timeline = this.timeline ??= await room!.getTimeline();
|
||||
|
||||
if (tabController.index == 0 && searchController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
searchStream = timeline
|
||||
.startSearch(
|
||||
searchTerm: searchController.text,
|
||||
prevBatch: prevBatch,
|
||||
requestHistoryCount: 1000,
|
||||
limit: 32,
|
||||
)
|
||||
.map(
|
||||
(result) => (
|
||||
[
|
||||
if (previousSearchResult != null) ...previousSearchResult,
|
||||
...result.$1,
|
||||
],
|
||||
result.$2,
|
||||
),
|
||||
)
|
||||
.asBroadcastStream();
|
||||
});
|
||||
}
|
||||
|
||||
void startGallerySearch({
|
||||
String? prevBatch,
|
||||
List<Event>? previousSearchResult,
|
||||
}) async {
|
||||
final timeline = this.timeline ??= await room!.getTimeline();
|
||||
|
||||
setState(() {
|
||||
galleryStream = timeline
|
||||
.startSearch(
|
||||
searchFunc: (event) => {
|
||||
MessageTypes.Image,
|
||||
MessageTypes.Video,
|
||||
}.contains(event.messageType),
|
||||
prevBatch: prevBatch,
|
||||
requestHistoryCount: 1000,
|
||||
limit: 32,
|
||||
)
|
||||
.map(
|
||||
(result) => (
|
||||
[
|
||||
if (previousSearchResult != null) ...previousSearchResult,
|
||||
...result.$1,
|
||||
],
|
||||
result.$2,
|
||||
),
|
||||
)
|
||||
.asBroadcastStream();
|
||||
});
|
||||
}
|
||||
|
||||
void startFileSearch({
|
||||
String? prevBatch,
|
||||
List<Event>? previousSearchResult,
|
||||
}) async {
|
||||
final timeline = this.timeline ??= await room!.getTimeline();
|
||||
|
||||
setState(() {
|
||||
fileStream = timeline
|
||||
.startSearch(
|
||||
searchFunc: (event) =>
|
||||
event.messageType == MessageTypes.File ||
|
||||
(event.messageType == MessageTypes.Audio &&
|
||||
!event.content.containsKey('org.matrix.msc3245.voice')),
|
||||
prevBatch: prevBatch,
|
||||
requestHistoryCount: 1000,
|
||||
limit: 32,
|
||||
)
|
||||
.map(
|
||||
(result) => (
|
||||
[
|
||||
if (previousSearchResult != null) ...previousSearchResult,
|
||||
...result.$1,
|
||||
],
|
||||
result.$2,
|
||||
),
|
||||
)
|
||||
.asBroadcastStream();
|
||||
});
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
switch (tabController.index) {
|
||||
case 1:
|
||||
startGallerySearch();
|
||||
break;
|
||||
case 2:
|
||||
startFileSearch();
|
||||
break;
|
||||
default:
|
||||
restartSearch();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tabController = TabController(initialIndex: 0, length: 3, vsync: this);
|
||||
tabController.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tabController.removeListener(_onTabChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatSearchView(this);
|
||||
}
|
||||
101
lib/pages/chat_search/chat_search_view.dart
Normal file
101
lib/pages/chat_search/chat_search_view.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_search/chat_search_files_tab.dart';
|
||||
import 'package:fluffychat/pages/chat_search/chat_search_images_tab.dart';
|
||||
import 'package:fluffychat/pages/chat_search/chat_search_message_tab.dart';
|
||||
import 'package:fluffychat/pages/chat_search/chat_search_page.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
class ChatSearchView extends StatelessWidget {
|
||||
final ChatSearchController controller;
|
||||
|
||||
const ChatSearchView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = controller.room;
|
||||
if (room == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child:
|
||||
Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const Center(child: BackButton()),
|
||||
titleSpacing: 0,
|
||||
title: Text(
|
||||
L10n.of(context)!.searchIn(
|
||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (FluffyThemes.isThreeColumnMode(context))
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
onSubmitted: (_) => controller.restartSearch(),
|
||||
autofocus: true,
|
||||
enabled: controller.tabController.index == 0,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!.search,
|
||||
suffixIcon: const Icon(Icons.search_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
TabBar(
|
||||
controller: controller.tabController,
|
||||
tabs: [
|
||||
Tab(child: Text(L10n.of(context)!.messages)),
|
||||
Tab(child: Text(L10n.of(context)!.gallery)),
|
||||
Tab(child: Text(L10n.of(context)!.files)),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
children: [
|
||||
ChatSearchMessageTab(
|
||||
searchQuery: controller.searchController.text,
|
||||
room: room,
|
||||
startSearch: controller.startMessageSearch,
|
||||
searchStream: controller.searchStream,
|
||||
),
|
||||
ChatSearchImagesTab(
|
||||
room: room,
|
||||
startSearch: controller.startGallerySearch,
|
||||
searchStream: controller.galleryStream,
|
||||
),
|
||||
ChatSearchFilesTab(
|
||||
room: room,
|
||||
startSearch: controller.startFileSearch,
|
||||
searchStream: controller.fileStream,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,21 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'matrix.dart';
|
||||
|
||||
enum ChatPopupMenuActions {
|
||||
details,
|
||||
mute,
|
||||
unmute,
|
||||
leave,
|
||||
search,
|
||||
// #Pangea
|
||||
archive,
|
||||
downloadTxt,
|
||||
downloadCsv,
|
||||
downloadXlsx,
|
||||
learningSettings,
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
class ChatSettingsPopupMenu extends StatefulWidget {
|
||||
final Room room;
|
||||
final bool displayChatDetails;
|
||||
|
|
@ -43,124 +58,18 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
// Pangea#
|
||||
notificationChangeSub ??= Matrix.of(context)
|
||||
.client
|
||||
.onAccountData
|
||||
.onSync
|
||||
.stream
|
||||
.where((u) => u.type == 'm.push_rules')
|
||||
.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.accountData?.any(
|
||||
(accountData) => accountData.type == 'm.push_rules',
|
||||
) ??
|
||||
false,
|
||||
)
|
||||
.listen(
|
||||
(u) => setState(() {}),
|
||||
);
|
||||
final items = <PopupMenuEntry<String>>[
|
||||
// #Pangea
|
||||
PopupMenuItem<String>(
|
||||
value: 'learning_settings',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.psychology_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.learningSettings),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
widget.room.pushRuleState == PushRuleState.notify
|
||||
? PopupMenuItem<String>(
|
||||
value: 'mute',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.notifications_off_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.muteChat),
|
||||
],
|
||||
),
|
||||
)
|
||||
: PopupMenuItem<String>(
|
||||
value: 'unmute',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.notifications_on_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.unmuteChat),
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
if (!widget.room.isArchived)
|
||||
if (widget.room.isRoomAdmin)
|
||||
PopupMenuItem<String>(
|
||||
value: 'archive',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.archive_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.archive),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
PopupMenuItem<String>(
|
||||
value: 'leave',
|
||||
child: Row(
|
||||
children: [
|
||||
// #Pangea
|
||||
// const Icon(Icons.delete_outlined),
|
||||
const Icon(Icons.arrow_forward),
|
||||
// Pangea#
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.leave),
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
if (classSettings != null)
|
||||
PopupMenuItem<String>(
|
||||
value: 'download txt',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.download_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.downloadTxtFile),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (classSettings != null)
|
||||
PopupMenuItem<String>(
|
||||
value: 'download csv',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.download_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.downloadCSVFile),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (classSettings != null)
|
||||
PopupMenuItem<String>(
|
||||
value: 'download xlsx',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.download_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.downloadXLSXFile),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
];
|
||||
if (widget.displayChatDetails) {
|
||||
items.insert(
|
||||
0,
|
||||
PopupMenuItem<String>(
|
||||
value: 'details',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.chatDetails),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
|
|
@ -175,11 +84,50 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
// child: const SizedBox.shrink(),
|
||||
// ),
|
||||
// Pangea#
|
||||
PopupMenuButton(
|
||||
onSelected: (String choice) async {
|
||||
PopupMenuButton<ChatPopupMenuActions>(
|
||||
onSelected: (choice) async {
|
||||
switch (choice) {
|
||||
case ChatPopupMenuActions.leave:
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.areYouSure,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
message: L10n.of(context)!.archiveRoomDescription,
|
||||
);
|
||||
if (confirmed == OkCancelResult.ok) {
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.room.leave(),
|
||||
);
|
||||
if (success.error == null) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ChatPopupMenuActions.mute:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.room.setPushRuleState(PushRuleState.mentionsOnly),
|
||||
);
|
||||
break;
|
||||
case ChatPopupMenuActions.unmute:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.room.setPushRuleState(PushRuleState.notify),
|
||||
);
|
||||
break;
|
||||
case ChatPopupMenuActions.details:
|
||||
_showChatDetails();
|
||||
break;
|
||||
case ChatPopupMenuActions.search:
|
||||
context.go('/rooms/${widget.room.id}/search');
|
||||
break;
|
||||
// #Pangea
|
||||
case 'archive':
|
||||
case ChatPopupMenuActions.archive:
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
|
|
@ -198,52 +146,7 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
}
|
||||
}
|
||||
break;
|
||||
// Pangea#
|
||||
case 'leave':
|
||||
final bool onlyAdmin = await widget.room.isOnlyAdmin();
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.areYouSure,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
message: onlyAdmin
|
||||
? L10n.of(context)!.onlyAdminDescription
|
||||
: L10n.of(context)!.leaveRoomDescription,
|
||||
);
|
||||
if (confirmed == OkCancelResult.ok) {
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
onlyAdmin ? widget.room.archive() : widget.room.leave(),
|
||||
);
|
||||
if (success.error == null) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'mute':
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.room.setPushRuleState(PushRuleState.mentionsOnly),
|
||||
);
|
||||
break;
|
||||
case 'unmute':
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.room.setPushRuleState(PushRuleState.notify),
|
||||
);
|
||||
break;
|
||||
case 'todos':
|
||||
context.go('/rooms/${widget.room.id}/tasks');
|
||||
break;
|
||||
case 'details':
|
||||
_showChatDetails();
|
||||
break;
|
||||
// #Pangea
|
||||
case 'download txt':
|
||||
case ChatPopupMenuActions.downloadTxt:
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => downloadChat(
|
||||
|
|
@ -255,7 +158,7 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
),
|
||||
);
|
||||
break;
|
||||
case 'download csv':
|
||||
case ChatPopupMenuActions.downloadCsv:
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => downloadChat(
|
||||
|
|
@ -267,7 +170,7 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
),
|
||||
);
|
||||
break;
|
||||
case 'download xlsx':
|
||||
case ChatPopupMenuActions.downloadXlsx:
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => downloadChat(
|
||||
|
|
@ -279,13 +182,129 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
),
|
||||
);
|
||||
break;
|
||||
case 'learning_settings':
|
||||
case ChatPopupMenuActions.learningSettings:
|
||||
context.go('/rooms/settings/learning');
|
||||
break;
|
||||
// Pangea#
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => items,
|
||||
itemBuilder: (BuildContext context) => [
|
||||
// #Pangea
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.learningSettings,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.psychology_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.learningSettings),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
if (widget.displayChatDetails)
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.details,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.chatDetails),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.room.pushRuleState == PushRuleState.notify)
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.mute,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.notifications_off_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.muteChat),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.unmute,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.notifications_on_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.unmuteChat),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.search,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.search),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.leave,
|
||||
child: Row(
|
||||
children: [
|
||||
// #Pangea
|
||||
// const Icon(Icons.delete_outlined),
|
||||
const Icon(Icons.arrow_forward),
|
||||
// Pangea#
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.leave),
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
if (!widget.room.isArchived)
|
||||
if (widget.room.isRoomAdmin)
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.archive,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.archive_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.archive),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (classSettings != null)
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.downloadTxt,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.download_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.downloadTxtFile),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (classSettings != null)
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.downloadCsv,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.download_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.downloadCSVFile),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (classSettings != null)
|
||||
PopupMenuItem<ChatPopupMenuActions>(
|
||||
value: ChatPopupMenuActions.downloadXlsx,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.download_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.downloadXLSXFile),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -769,6 +769,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -2228,6 +2232,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -3711,6 +3719,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -5198,6 +5210,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -6112,6 +6128,10 @@
|
|||
"createNewAddress",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -7058,6 +7078,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -7946,6 +7970,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -9356,6 +9384,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -10492,6 +10524,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -10636,6 +10672,10 @@
|
|||
"createNewAddress",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"addSpaceToSpaceDescription",
|
||||
"noDatabaseEncryption",
|
||||
"thereAreCountUsersBlocked"
|
||||
|
|
@ -11411,6 +11451,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -12271,6 +12315,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -13236,6 +13284,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -14193,6 +14245,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -15506,6 +15562,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -16498,6 +16558,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -17619,6 +17683,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -18507,6 +18575,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -19719,6 +19791,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -21199,6 +21275,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -22144,6 +22224,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -23040,6 +23124,10 @@
|
|||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -24487,6 +24575,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -25375,6 +25467,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -26591,6 +26687,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -27511,6 +27611,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
"signUp",
|
||||
|
|
@ -28524,6 +28628,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -29865,6 +29973,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -30753,6 +30865,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -31747,6 +31863,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -32637,6 +32757,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -33795,6 +33919,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -34745,6 +34873,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -35704,6 +35836,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -37169,6 +37305,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -38057,6 +38197,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -39216,6 +39360,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -40210,6 +40358,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -41098,6 +41250,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -42323,6 +42479,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -43706,6 +43866,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -44863,6 +45027,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -45777,6 +45945,10 @@
|
|||
"createNewAddress",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -47238,6 +47410,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -48676,6 +48852,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -49564,6 +49744,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -50450,6 +50634,10 @@
|
|||
"createNewAddress",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -51840,6 +52028,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
@ -52728,6 +52920,10 @@
|
|||
"clickToManageSubscription",
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"signUp",
|
||||
"pleaseChooseAtLeastChars",
|
||||
"noEmailWarning",
|
||||
|
|
@ -53836,6 +54032,10 @@
|
|||
"initAppError",
|
||||
"userRole",
|
||||
"minimumPowerLevel",
|
||||
"searchIn",
|
||||
"searchMore",
|
||||
"gallery",
|
||||
"files",
|
||||
"databaseBuildErrorBody",
|
||||
"sessionLostBody",
|
||||
"restoreSessionBody",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue