Compare commits

...

4 Commits

Author SHA1 Message Date
LittleSheep
c9b71701c8 🚀 Launch 3.2.0+129 2025-08-26 01:33:57 +08:00
LittleSheep
28e98488f1 🌐 Localize new stuff 2025-08-26 01:24:58 +08:00
LittleSheep
b4d476613e 🐛 Optimzation and bug fixes 2025-08-26 01:18:54 +08:00
LittleSheep
b48a1aac44 Search messages!
♻️ Optimize messages loading, syncing
2025-08-26 01:05:30 +08:00
16 changed files with 582 additions and 73 deletions

View File

@@ -944,5 +944,21 @@
"unpinPostHint": "Are you sure you want to unpin this post?", "unpinPostHint": "Are you sure you want to unpin this post?",
"all": "All", "all": "All",
"statusPresent": "Present", "statusPresent": "Present",
"accountAutomated": "Automated" "accountAutomated": "Automated",
"chatBreakClearButton": "Clear",
"chatBreak5m": "5m",
"chatBreak10m": "10m",
"chatBreak15m": "15m",
"chatBreak30m": "30m",
"chatBreakCustomMinutes": "Custom (minutes)",
"chatBreakEnterMinutes": "Enter minutes",
"errorGeneric": "Error: {}",
"searchMessages": "Search Messages",
"messagesCount": "{} messages",
"dotSeparator": "·",
"roleValidationHint": "Role must be between 0 and 100",
"searchMessagesHint": "Search messages...",
"searchLinks": "Links",
"searchAttachments": "Attachments",
"noMessagesFound": "No messages found"
} }

View File

@@ -40,6 +40,8 @@ PODS:
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/CoreOnly (12.0.0): - Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0) - FirebaseCore (~> 12.0.0)
- Firebase/Crashlytics (12.0.0): - Firebase/Crashlytics (12.0.0):
@@ -303,6 +305,7 @@ DEPENDENCIES:
- croppy (from `.symlinks/plugins/croppy/ios`) - croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
@@ -381,6 +384,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
file_saver:
:path: ".symlinks/plugins/file_saver/ios"
firebase_analytics: firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios" :path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core: firebase_core:
@@ -464,6 +469,7 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4

View File

@@ -68,6 +68,34 @@ class AppDatabase extends _$AppDatabase {
return (delete(chatMessages)..where((m) => m.id.equals(id))).go(); return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
} }
Future<int> getTotalMessagesForRoom(String roomId) {
return (select(chatMessages)..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length);
}
Future<List<LocalChatMessage>> searchMessages(
String roomId,
String query,
) async {
var selectStatement = select(chatMessages)
..where((m) => m.roomId.equals(roomId));
if (query.isNotEmpty) {
selectStatement =
selectStatement
..where((m) => m.content.like('%${query.toLowerCase()}%'));
}
final messages =
await (selectStatement
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
.get();
return messages.map((msg) => companionToMessage(msg)).toList();
}
// Convert between Drift and model objects // Convert between Drift and model objects
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) { ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
return ChatMessagesCompanion( return ChatMessagesCompanion(

View File

@@ -8,7 +8,7 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';

View File

@@ -38,6 +38,7 @@ import 'package:island/screens/chat/chat.dart';
import 'package:island/screens/chat/room.dart'; import 'package:island/screens/chat/room.dart';
import 'package:island/screens/chat/room_detail.dart'; import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.dart'; import 'package:island/screens/chat/call.dart';
import 'package:island/screens/chat/search_messages_screen.dart';
import 'package:island/screens/creators/hub.dart'; import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart'; import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart'; import 'package:island/screens/creators/stickers/stickers.dart';
@@ -555,6 +556,14 @@ final routerProvider = Provider<GoRouter>((ref) {
return ChatDetailScreen(id: id); return ChatDetailScreen(id: id);
}, },
), ),
GoRoute(
name: 'searchMessages',
path: '/chat/:id/search',
builder: (context, state) {
final id = state.pathParameters['id']!;
return SearchMessagesScreen(roomId: id);
},
),
], ],
), ),

View File

@@ -95,8 +95,24 @@ class LevelingScreen extends HookConsumerWidget {
title: Text('levelingProgress'.tr()), title: Text('levelingProgress'.tr()),
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(text: 'leveling'.tr()), Tab(
Tab(text: 'stellarProgram'.tr()), child: Text(
'leveling'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'stellarProgram'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
], ],
), ),
), ),

View File

@@ -283,6 +283,9 @@ class MessagesNotifier extends _$MessagesNotifier {
final Map<String, LocalChatMessage> _pendingMessages = {}; final Map<String, LocalChatMessage> _pendingMessages = {};
final Map<String, Map<int, double>> _fileUploadProgress = {}; final Map<String, Map<int, double>> _fileUploadProgress = {};
int? _totalCount; int? _totalCount;
String? _searchQuery;
bool? _withLinks;
bool? _withAttachments;
late final String _roomId; late final String _roomId;
int _currentPage = 0; int _currentPage = 0;
@@ -326,7 +329,13 @@ class MessagesNotifier extends _$MessagesNotifier {
}); });
} }
return await loadInitial(); loadInitial();
return [];
}
List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) {
messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return messages;
} }
Future<List<LocalChatMessage>> _getCachedMessages({ Future<List<LocalChatMessage>> _getCachedMessages({
@@ -337,13 +346,32 @@ class MessagesNotifier extends _$MessagesNotifier {
'Getting cached messages from offset $offset, take $take', 'Getting cached messages from offset $offset, take $take',
name: 'MessagesNotifier', name: 'MessagesNotifier',
); );
final dbMessages = await _database.getMessagesForRoom( final List<LocalChatMessage> dbMessages;
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
dbMessages = await _database.searchMessages(_roomId, _searchQuery ?? '');
} else {
final chatMessagesFromDb = await _database.getMessagesForRoom(
_roomId, _roomId,
offset: offset, offset: offset,
limit: take, limit: take,
); );
final dbLocalMessages = dbMessages =
dbMessages.map(_database.companionToMessage).toList(); chatMessagesFromDb.map(_database.companionToMessage).toList();
}
List<LocalChatMessage> filteredMessages = dbMessages;
if (_withLinks == true) {
filteredMessages =
filteredMessages.where((msg) => _hasLink(msg)).toList();
}
if (_withAttachments == true) {
filteredMessages =
filteredMessages.where((msg) => _hasAttachment(msg)).toList();
}
final dbLocalMessages = filteredMessages;
if (offset == 0) { if (offset == 0) {
final pendingForRoom = final pendingForRoom =
@@ -352,7 +380,7 @@ class MessagesNotifier extends _$MessagesNotifier {
.toList(); .toList();
final allMessages = [...pendingForRoom, ...dbLocalMessages]; final allMessages = [...pendingForRoom, ...dbLocalMessages];
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); _sortMessages(allMessages); // Use the helper function
final uniqueMessages = <LocalChatMessage>[]; final uniqueMessages = <LocalChatMessage>[];
final seenIds = <String>{}; final seenIds = <String>{};
@@ -427,7 +455,7 @@ class MessagesNotifier extends _$MessagesNotifier {
_isSyncing = true; _isSyncing = true;
developer.log('Starting message sync', name: 'MessagesNotifier'); developer.log('Starting message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = true; Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
try { try {
final dbMessages = await _database.getMessagesForRoom( final dbMessages = await _database.getMessagesForRoom(
_room.id, _room.id,
@@ -488,7 +516,9 @@ class MessagesNotifier extends _$MessagesNotifier {
showErrorAlert(err); showErrorAlert(err);
} finally { } finally {
developer.log('Finished message sync', name: 'MessagesNotifier'); developer.log('Finished message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = false; Future.microtask(
() => ref.read(isSyncingProvider.notifier).state = false,
);
_isSyncing = false; _isSyncing = false;
} }
} }
@@ -499,7 +529,9 @@ class MessagesNotifier extends _$MessagesNotifier {
bool synced = false, bool synced = false,
}) async { }) async {
try { try {
if (offset == 0 && !synced) { if (offset == 0 &&
!synced &&
(_searchQuery == null || _searchQuery!.isEmpty)) {
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) { _fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
return <LocalChatMessage>[]; return <LocalChatMessage>[];
}); });
@@ -514,7 +546,11 @@ class MessagesNotifier extends _$MessagesNotifier {
return localMessages; return localMessages;
} }
if (_searchQuery == null || _searchQuery!.isEmpty) {
return await _fetchAndCacheMessages(offset: offset, take: take); return await _fetchAndCacheMessages(offset: offset, take: take);
} else {
return []; // If searching, and no local messages, don't fetch from network
}
} catch (e) { } catch (e) {
final localMessages = await _getCachedMessages( final localMessages = await _getCachedMessages(
offset: offset, offset: offset,
@@ -528,13 +564,15 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
} }
Future<List<LocalChatMessage>> loadInitial() async { Future<void> loadInitial() async {
developer.log('Loading initial messages', name: 'MessagesNotifier'); developer.log('Loading initial messages', name: 'MessagesNotifier');
if (_searchQuery == null || _searchQuery!.isEmpty) {
syncMessages(); syncMessages();
}
final messages = await _getCachedMessages(offset: 0, take: 100); final messages = await _getCachedMessages(offset: 0, take: 100);
_currentPage = 0; _currentPage = 0;
_hasMore = messages.length == _pageSize; _hasMore = messages.length == _pageSize;
return messages; state = AsyncValue.data(messages);
} }
Future<void> loadMore() async { Future<void> loadMore() async {
@@ -553,7 +591,9 @@ class MessagesNotifier extends _$MessagesNotifier {
_hasMore = false; _hasMore = false;
} }
state = AsyncValue.data([...currentMessages, ...newMessages]); state = AsyncValue.data(
_sortMessages([...currentMessages, ...newMessages]),
);
} catch (err, stackTrace) { } catch (err, stackTrace) {
developer.log( developer.log(
'Error loading more messages', 'Error loading more messages',
@@ -778,7 +818,7 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
return m; return m;
}).toList(); }).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(_sortMessages(newMessages));
showErrorAlert(e); showErrorAlert(e);
} }
} }
@@ -838,7 +878,7 @@ class MessagesNotifier extends _$MessagesNotifier {
if (index >= 0) { if (index >= 0) {
final newList = [...currentMessages]; final newList = [...currentMessages];
newList[index] = updatedMessage; newList[index] = updatedMessage;
state = AsyncValue.data(newList); state = AsyncValue.data(_sortMessages(newList));
} }
} }
@@ -898,6 +938,20 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
} }
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
_searchQuery = query.trim();
_withLinks = withLinks;
_withAttachments = withAttachments;
loadInitial();
}
void clearSearch() {
_searchQuery = null;
_withLinks = null;
_withAttachments = null;
loadInitial();
}
Future<LocalChatMessage?> fetchMessageById(String messageId) async { Future<LocalChatMessage?> fetchMessageById(String messageId) async {
developer.log( developer.log(
'Fetching message by id $messageId', 'Fetching message by id $messageId',
@@ -927,6 +981,18 @@ class MessagesNotifier extends _$MessagesNotifier {
rethrow; rethrow;
} }
} }
bool _hasLink(LocalChatMessage message) {
final content = message.toRemoteMessage().content;
if (content == null) return false;
final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*');
return urlRegex.hasMatch(content);
}
bool _hasAttachment(LocalChatMessage message) {
final remoteMessage = message.toRemoteMessage();
return remoteMessage.attachments.isNotEmpty;
}
} }
class ChatRoomScreen extends HookConsumerWidget { class ChatRoomScreen extends HookConsumerWidget {

View File

@@ -6,7 +6,7 @@ part of 'room.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$messagesNotifierHash() => r'dda98f5bf525f3b2bc0a7c89bc6eaa3c8b95f142'; String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -20,10 +20,17 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/database.dart';
part 'room_detail.freezed.dart'; part 'room_detail.freezed.dart';
part 'room_detail.g.dart'; part 'room_detail.g.dart';
@riverpod
Future<int> totalMessagesCount(Ref ref, String roomId) async {
final database = ref.watch(databaseProvider);
return database.getTotalMessagesForRoom(roomId);
}
class ChatDetailScreen extends HookConsumerWidget { class ChatDetailScreen extends HookConsumerWidget {
final String id; final String id;
const ChatDetailScreen({super.key, required this.id}); const ChatDetailScreen({super.key, required this.id});
@@ -32,6 +39,7 @@ class ChatDetailScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final roomState = ref.watch(chatroomProvider(id)); final roomState = ref.watch(chatroomProvider(id));
final roomIdentity = ref.watch(chatroomIdentityProvider(id)); final roomIdentity = ref.watch(chatroomIdentityProvider(id));
final totalMessages = ref.watch(totalMessagesCountProvider(id));
const kNotifyLevelText = [ const kNotifyLevelText = [
'chatNotifyLevelAll', 'chatNotifyLevelAll',
@@ -132,7 +140,7 @@ class ChatDetailScreen extends HookConsumerWidget {
const Text('chatBreakDescription').tr(), const Text('chatBreakDescription').tr(),
const Gap(16), const Gap(16),
ListTile( ListTile(
title: const Text('Clear').tr(), title: const Text('chatBreakClearButton').tr(),
subtitle: const Text('chatBreakClear').tr(), subtitle: const Text('chatBreakClear').tr(),
leading: const Icon(Icons.notifications_active), leading: const Icon(Icons.notifications_active),
onTap: () { onTap: () {
@@ -144,8 +152,8 @@ class ChatDetailScreen extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: const Text('5m'), title: const Text('chatBreak5m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['5m']), subtitle: const Text('chatBreakHour').tr(args: ['chatBreak5m'.tr()]),
leading: const Icon(Symbols.circle), leading: const Icon(Symbols.circle),
onTap: () { onTap: () {
setChatBreak(now.add(const Duration(minutes: 5))); setChatBreak(now.add(const Duration(minutes: 5)));
@@ -156,8 +164,8 @@ class ChatDetailScreen extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: const Text('10m'), title: const Text('chatBreak10m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['10m']), subtitle: const Text('chatBreakHour').tr(args: ['chatBreak10m'.tr()]),
leading: const Icon(Symbols.circle), leading: const Icon(Symbols.circle),
onTap: () { onTap: () {
setChatBreak(now.add(const Duration(minutes: 10))); setChatBreak(now.add(const Duration(minutes: 10)));
@@ -168,8 +176,8 @@ class ChatDetailScreen extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: const Text('15m'), title: const Text('chatBreak15m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['15m']), subtitle: const Text('chatBreakHour').tr(args: ['chatBreak15m'.tr()]),
leading: const Icon(Symbols.timer_3), leading: const Icon(Symbols.timer_3),
onTap: () { onTap: () {
setChatBreak(now.add(const Duration(minutes: 15))); setChatBreak(now.add(const Duration(minutes: 15)));
@@ -180,8 +188,8 @@ class ChatDetailScreen extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: const Text('30m'), title: const Text('chatBreak30m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['30m']), subtitle: const Text('chatBreakHour').tr(args: ['chatBreak30m'.tr()]),
leading: const Icon(Symbols.timer), leading: const Icon(Symbols.timer),
onTap: () { onTap: () {
setChatBreak(now.add(const Duration(minutes: 30))); setChatBreak(now.add(const Duration(minutes: 30)));
@@ -195,8 +203,8 @@ class ChatDetailScreen extends HookConsumerWidget {
TextField( TextField(
controller: durationController, controller: durationController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Custom (minutes)'.tr(), labelText: 'chatBreakCustomMinutes'.tr(),
hintText: 'Enter minutes'.tr(), hintText: 'chatBreakEnterMinutes'.tr(),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
@@ -239,7 +247,7 @@ class ChatDetailScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
body: roomState.when( body: roomState.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')), error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
data: data:
(currentRoom) => CustomScrollView( (currentRoom) => CustomScrollView(
slivers: [ slivers: [
@@ -359,6 +367,22 @@ class ChatDetailScreen extends HookConsumerWidget {
: const Text('chatBreakNone').tr(), : const Text('chatBreakNone').tr(),
onTap: () => showChatBreakDialog(), onTap: () => showChatBreakDialog(),
), ),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.search),
trailing: const Icon(Symbols.chevron_right),
title: const Text('searchMessages').tr(),
subtitle: totalMessages.when(
data: (count) => Text('messagesCount'.tr(args: [count.toString()])),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('errorGeneric'.tr(args: [err.toString()])),
),
onTap: () {
context.pushNamed('searchMessages', pathParameters: {'id': id});
},
),
], ],
), ),
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
@@ -692,7 +716,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
? 'permissionModerator' ? 'permissionModerator'
: 'permissionMember', : 'permissionMember',
).tr(), ).tr(),
Text('·').bold().padding(horizontal: 6), Text('dotSeparator').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account.name}")), Expanded(child: Text("@${member.account.name}")),
], ],
), ),
@@ -852,7 +876,7 @@ class _ChatMemberRoleSheet extends HookConsumerWidget {
try { try {
final newRole = int.parse(roleController.text); final newRole = int.parse(roleController.text);
if (newRole < 0 || newRole > 100) { if (newRole < 0 || newRole > 100) {
throw 'Role must be between 0 and 100'; throw 'roleValidationHint'.tr();
} }
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);

View File

@@ -6,8 +6,8 @@ part of 'room_detail.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$chatMemberListNotifierHash() => String _$totalMessagesCountHash() =>
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973'; r'a15c03461f25c2d4d39c0926509bf626ae2550a6';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -30,6 +30,128 @@ class _SystemHash {
} }
} }
/// See also [totalMessagesCount].
@ProviderFor(totalMessagesCount)
const totalMessagesCountProvider = TotalMessagesCountFamily();
/// See also [totalMessagesCount].
class TotalMessagesCountFamily extends Family<AsyncValue<int>> {
/// See also [totalMessagesCount].
const TotalMessagesCountFamily();
/// See also [totalMessagesCount].
TotalMessagesCountProvider call(String roomId) {
return TotalMessagesCountProvider(roomId);
}
@override
TotalMessagesCountProvider getProviderOverride(
covariant TotalMessagesCountProvider provider,
) {
return call(provider.roomId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'totalMessagesCountProvider';
}
/// See also [totalMessagesCount].
class TotalMessagesCountProvider extends AutoDisposeFutureProvider<int> {
/// See also [totalMessagesCount].
TotalMessagesCountProvider(String roomId)
: this._internal(
(ref) => totalMessagesCount(ref as TotalMessagesCountRef, roomId),
from: totalMessagesCountProvider,
name: r'totalMessagesCountProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$totalMessagesCountHash,
dependencies: TotalMessagesCountFamily._dependencies,
allTransitiveDependencies:
TotalMessagesCountFamily._allTransitiveDependencies,
roomId: roomId,
);
TotalMessagesCountProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.roomId,
}) : super.internal();
final String roomId;
@override
Override overrideWith(
FutureOr<int> Function(TotalMessagesCountRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TotalMessagesCountProvider._internal(
(ref) => create(ref as TotalMessagesCountRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
roomId: roomId,
),
);
}
@override
AutoDisposeFutureProviderElement<int> createElement() {
return _TotalMessagesCountProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TotalMessagesCountProvider && other.roomId == roomId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, roomId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin TotalMessagesCountRef on AutoDisposeFutureProviderRef<int> {
/// The parameter `roomId` of this provider.
String get roomId;
}
class _TotalMessagesCountProviderElement
extends AutoDisposeFutureProviderElement<int>
with TotalMessagesCountRef {
_TotalMessagesCountProviderElement(super.provider);
@override
String get roomId => (origin as TotalMessagesCountProvider).roomId;
}
String _$chatMemberListNotifierHash() =>
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
abstract class _$ChatMemberListNotifier abstract class _$ChatMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> { extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> {
late final String roomId; late final String roomId;

View File

@@ -0,0 +1,139 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/message_item.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class SearchMessagesScreen extends HookConsumerWidget {
final String roomId;
const SearchMessagesScreen({super.key, required this.roomId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchController = useTextEditingController();
final withLinks = useState(false);
final withAttachments = useState(false);
final messagesNotifier = ref.read(
messagesNotifierProvider(roomId).notifier,
);
final messages = ref.watch(messagesNotifierProvider(roomId));
useEffect(() {
// Clear search when screen is disposed
return () {
messagesNotifier.clearSearch();
};
}, []);
return AppScaffold(
appBar: AppBar(title: const Text('searchMessages').tr()),
body: Column(
children: [
Column(
children: [
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'searchMessagesHint'.tr(),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 16,
),
suffix: IconButton(
iconSize: 18,
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
messagesNotifier.clearSearch();
},
),
),
onChanged: (query) {
messagesNotifier.searchMessages(
query,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
Row(
children: [
Expanded(
child: CheckboxListTile(
secondary: const Icon(Symbols.link),
title: const Text('searchLinks').tr(),
value: withLinks.value,
onChanged: (bool? value) {
withLinks.value = value!;
messagesNotifier.searchMessages(
searchController.text,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
),
Expanded(
child: CheckboxListTile(
secondary: const Icon(Symbols.file_copy),
title: const Text('searchAttachments').tr(),
value: withAttachments.value,
onChanged: (bool? value) {
withAttachments.value = value!;
messagesNotifier.searchMessages(
searchController.text,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
),
],
),
],
),
const Divider(height: 1),
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('noMessagesFound'.tr()))
: SuperListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
itemBuilder: (context, index) {
final message = messageList[index];
// Simplified MessageItem for search results, no grouping logic
return MessageItem(
message: message,
isCurrentUser:
false, // Or determine based on actual user
onAction: null,
onJump: (_) {},
progress: null,
showAvatar: true,
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
),
),
],
),
);
}
}

View File

@@ -27,7 +27,9 @@ class AppDetailScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController(initialLength: 2); final tabController = useTabController(initialLength: 2);
final appData = ref.watch(customAppProvider(publisherName, projectId, appId)); final appData = ref.watch(
customAppProvider(publisherName, projectId, appId),
);
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
@@ -35,7 +37,8 @@ class AppDetailScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.edit), icon: const Icon(Symbols.edit),
onPressed: appData.value == null onPressed:
appData.value == null
? null ? null
: () { : () {
context.pushNamed( context.pushNamed(
@@ -51,7 +54,26 @@ class AppDetailScreen extends HookConsumerWidget {
], ],
bottom: TabBar( bottom: TabBar(
controller: tabController, controller: tabController,
tabs: [Tab(text: 'overview'.tr()), Tab(text: 'secrets'.tr())], tabs: [
Tab(
child: Text(
'overview'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'secrets'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
), ),
), ),
body: appData.when( body: appData.when(
@@ -70,9 +92,11 @@ class AppDetailScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => ResponseErrorWidget( error:
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: () => ref.invalidate( onRetry:
() => ref.invalidate(
customAppProvider(publisherName, projectId, appId), customAppProvider(publisherName, projectId, appId),
), ),
), ),
@@ -98,7 +122,8 @@ class _AppOverview extends StatelessWidget {
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: app.background != null child:
app.background != null
? CloudFileWidget( ? CloudFileWidget(
item: app.background!, item: app.background!,
fit: BoxFit.cover, fit: BoxFit.cover,

View File

@@ -52,7 +52,26 @@ class BotDetailScreen extends HookConsumerWidget {
], ],
bottom: TabBar( bottom: TabBar(
controller: tabController, controller: tabController,
tabs: [Tab(text: 'overview'.tr()), Tab(text: 'keys'.tr())], tabs: [
Tab(
child: Text(
'overview'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'keys'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
), ),
), ),
body: botData.when( body: botData.when(

View File

@@ -58,7 +58,26 @@ class ProjectDetailScreen extends HookConsumerWidget {
], ],
bottom: TabBar( bottom: TabBar(
controller: tabController, controller: tabController,
tabs: [Tab(text: 'customApps'.tr()), Tab(text: 'bots'.tr())], tabs: [
Tab(
child: Text(
'customApps'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'bots'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
), ),
), ),
body: TabBarView( body: TabBarView(

View File

@@ -143,8 +143,26 @@ class ArticlesScreen extends ConsumerWidget {
bottom: TabBar( bottom: TabBar(
isScrollable: true, isScrollable: true,
tabs: [ tabs: [
const Tab(text: 'All'), Tab(
...feeds.map((feed) => Tab(text: feed.title)), child: Text(
'All',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
...feeds.map(
(feed) => Tab(
child: Text(
feed.title,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
),
], ],
), ),
), ),

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 3.2.0+128 version: 3.2.0+129
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2
@@ -139,6 +139,7 @@ dependencies:
material_color_utilities: ^0.11.1 material_color_utilities: ^0.11.1
screenshot: ^3.0.0 screenshot: ^3.0.0
flutter_card_swiper: ^7.0.2 flutter_card_swiper: ^7.0.2
file_saver: ^0.3.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -236,3 +237,4 @@ msix_config:
msix_version: 3.2.0.0 msix_version: 3.2.0.0
logo_path: .\assets\icons\icon.png logo_path: .\assets\icons\icon.png
capabilities: internetClientServer, location, microphone, webcam capabilities: internetClientServer, location, microphone, webcam