Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
5663df6ef1
|
|||
|
e996a0c95f
|
|||
|
a090e93f57
|
|||
|
c69034c071
|
|||
|
369ea6cf5b
|
|||
|
2e371b5296
|
|||
|
2e9d61bcfa
|
|||
|
9c2b5b0dfa
|
|||
|
3b40f515b3
|
|||
|
5ee61dbef2
|
|||
|
b151ef6686
|
|||
|
ff934d0f08
|
@@ -449,4 +449,13 @@ class AppDatabase extends _$AppDatabase {
|
||||
chatMembers,
|
||||
).insert(companionFromMember(member), mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
|
||||
Future<int> saveMessageWithSender(LocalChatMessage message) async {
|
||||
// First save the sender if it exists
|
||||
if (message.sender != null) {
|
||||
await saveMember(message.sender!);
|
||||
}
|
||||
// Then save the message
|
||||
return await saveMessage(messageToCompanion(message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$callNotifierHash() => r'2caee30f42315e539cb4df17c0d464ceed41ffa0';
|
||||
String _$callNotifierHash() => r'ef4e3e9c9d411cf9dce1ceb456a3b866b2c87db3';
|
||||
|
||||
/// See also [CallNotifier].
|
||||
@ProviderFor(CallNotifier)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
@@ -6,6 +8,58 @@ import 'package:island/pods/chat/chat_subscribe.dart';
|
||||
|
||||
part 'chat_summary.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatUnreadCountNotifier extends _$ChatUnreadCountNotifier {
|
||||
StreamSubscription<WebSocketPacket>? _subscription;
|
||||
|
||||
@override
|
||||
Future<int> build() async {
|
||||
// Subscribe to websocket events when this provider is built
|
||||
_subscribeToWebSocket();
|
||||
|
||||
// Dispose the subscription when this provider is disposed
|
||||
ref.onDispose(() {
|
||||
_subscription?.cancel();
|
||||
});
|
||||
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/sphere/chat/unread');
|
||||
return (response.data as num).toInt();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void _subscribeToWebSocket() {
|
||||
final webSocketService = ref.read(websocketProvider);
|
||||
_subscription = webSocketService.dataStream.listen((packet) {
|
||||
if (packet.type == 'messages.new' && packet.data != null) {
|
||||
final message = SnChatMessage.fromJson(packet.data!);
|
||||
final currentSubscribed = ref.read(currentSubscribedChatIdProvider);
|
||||
// Only increment if the message is not from the currently subscribed chat
|
||||
if (message.chatRoomId != currentSubscribed) {
|
||||
_incrementCounter();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _incrementCounter() async {
|
||||
final current = await future;
|
||||
state = AsyncData(current + 1);
|
||||
}
|
||||
|
||||
Future<void> decrement(int count) async {
|
||||
final current = await future;
|
||||
state = AsyncData(math.max(current - count, 0));
|
||||
}
|
||||
|
||||
void clear() async {
|
||||
state = AsyncData(0);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ChatSummary extends _$ChatSummary {
|
||||
@override
|
||||
@@ -41,6 +95,14 @@ class ChatSummary extends _$ChatSummary {
|
||||
state.whenData((summaries) {
|
||||
final summary = summaries[chatId];
|
||||
if (summary != null) {
|
||||
// Decrement global unread count
|
||||
final unreadToDecrement = summary.unreadCount;
|
||||
if (unreadToDecrement > 0) {
|
||||
ref
|
||||
.read(chatUnreadCountNotifierProvider.notifier)
|
||||
.decrement(unreadToDecrement);
|
||||
}
|
||||
|
||||
state = AsyncData({
|
||||
...summaries,
|
||||
chatId: SnChatSummary(
|
||||
|
||||
@@ -6,6 +6,24 @@ part of 'chat_summary.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatUnreadCountNotifierHash() =>
|
||||
r'b8d93589dc37f772d4c3a07d9afd81c37026e57d';
|
||||
|
||||
/// See also [ChatUnreadCountNotifier].
|
||||
@ProviderFor(ChatUnreadCountNotifier)
|
||||
final chatUnreadCountNotifierProvider =
|
||||
AutoDisposeAsyncNotifierProvider<ChatUnreadCountNotifier, int>.internal(
|
||||
ChatUnreadCountNotifier.new,
|
||||
name: r'chatUnreadCountNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatUnreadCountNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ChatUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
|
||||
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
|
||||
|
||||
/// See also [ChatSummary].
|
||||
|
||||
@@ -45,6 +45,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
bool _isSyncing = false;
|
||||
bool _isJumping = false;
|
||||
bool _isUpdatingState = false;
|
||||
bool _allRemoteMessagesFetched = false;
|
||||
DateTime? _lastPauseTime;
|
||||
|
||||
late final Future<SnAccount?> Function(String) _fetchAccount;
|
||||
@@ -278,6 +279,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
if (offset >= _totalCount!) {
|
||||
_allRemoteMessagesFetched = true;
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -299,10 +301,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}).toList();
|
||||
|
||||
for (final message in messages) {
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
if (message.sender != null) {
|
||||
await _database.saveMember(message.sender!); // Save/update member data
|
||||
}
|
||||
await _database.saveMessageWithSender(message);
|
||||
if (message.nonce != null) {
|
||||
_pendingMessages.removeWhere(
|
||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||
@@ -310,6 +309,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've fetched all remote messages
|
||||
if (offset + messages.length >= _totalCount!) {
|
||||
_allRemoteMessagesFetched = true;
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -319,6 +323,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
return;
|
||||
}
|
||||
_isSyncing = true;
|
||||
_allRemoteMessagesFetched = false;
|
||||
|
||||
talker.log('Starting message sync');
|
||||
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||
@@ -346,19 +351,48 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync with pagination support using timestamp-based cursor
|
||||
int? totalMessages;
|
||||
int syncedCount = 0;
|
||||
int lastSyncTimestamp =
|
||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch;
|
||||
|
||||
do {
|
||||
final resp = await _apiClient.post(
|
||||
'/sphere/chat/${_room.id}/sync',
|
||||
data: {
|
||||
'last_sync_timestamp':
|
||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
||||
},
|
||||
data: {'last_sync_timestamp': lastSyncTimestamp},
|
||||
);
|
||||
|
||||
// Read total count from header on first request
|
||||
if (totalMessages == null) {
|
||||
totalMessages = int.parse(
|
||||
resp.headers['x-total']?.firstOrNull ?? '0',
|
||||
);
|
||||
talker.log('Total messages to sync: $totalMessages');
|
||||
}
|
||||
|
||||
final response = MessageSyncResponse.fromJson(resp.data);
|
||||
talker.log('Sync response: ${response.messages.length} changes');
|
||||
final messagesCount = response.messages.length;
|
||||
talker.log(
|
||||
'Sync page: synced=$syncedCount/$totalMessages, count=$messagesCount',
|
||||
);
|
||||
|
||||
for (final message in response.messages) {
|
||||
await receiveMessage(message);
|
||||
}
|
||||
|
||||
syncedCount += messagesCount;
|
||||
|
||||
// Update cursor to the last message's createdAt for next page
|
||||
if (response.messages.isNotEmpty) {
|
||||
lastSyncTimestamp =
|
||||
response.messages.last.createdAt.millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
// Continue if there are more messages to fetch
|
||||
} while (syncedCount < totalMessages);
|
||||
|
||||
talker.log('Sync complete: synced $syncedCount messages');
|
||||
} catch (err, stackTrace) {
|
||||
talker.log(
|
||||
'Error syncing messages',
|
||||
@@ -397,14 +431,35 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
withAttachments: _withAttachments,
|
||||
);
|
||||
|
||||
if (localMessages.isNotEmpty) {
|
||||
// If we have local messages AND we've fetched all remote messages, return local
|
||||
if (localMessages.isNotEmpty && _allRemoteMessagesFetched) {
|
||||
return localMessages;
|
||||
}
|
||||
|
||||
// If we haven't fetched all remote messages, check remote even if we have local
|
||||
// OR if we have no local messages at all
|
||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||
return await _fetchAndCacheMessages(offset: offset, take: take);
|
||||
final remoteMessages = await _fetchAndCacheMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
);
|
||||
|
||||
// If we got remote messages, re-fetch from cache to get merged result
|
||||
if (remoteMessages.isNotEmpty) {
|
||||
return await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
searchQuery: _searchQuery,
|
||||
withLinks: _withLinks,
|
||||
withAttachments: _withAttachments,
|
||||
);
|
||||
}
|
||||
|
||||
// No remote messages, return local (if any)
|
||||
return localMessages;
|
||||
} else {
|
||||
return []; // If searching, and no local messages, don't fetch from network
|
||||
// For search queries, return local only
|
||||
return localMessages;
|
||||
}
|
||||
} catch (e) {
|
||||
final localMessages = await _getCachedMessages(
|
||||
@@ -424,6 +479,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
Future<void> loadInitial() async {
|
||||
talker.log('Loading initial messages');
|
||||
_allRemoteMessagesFetched = false;
|
||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||
syncMessages();
|
||||
}
|
||||
@@ -445,6 +501,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
if (!_hasMore || state is AsyncLoading) return;
|
||||
talker.log('Loading more messages');
|
||||
|
||||
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||
try {
|
||||
final currentMessages = state.value ?? [];
|
||||
final offset = currentMessages.length;
|
||||
@@ -466,6 +523,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
Future.microtask(
|
||||
() => ref.read(isSyncingProvider.notifier).state = false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,8 +562,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
_pendingMessages[localMessage.id] = localMessage;
|
||||
_fileUploadProgress[localMessage.id] = {};
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
await _database.saveMember(mockMessage.sender);
|
||||
await _database.saveMessageWithSender(localMessage);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||
@@ -553,7 +613,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
_pendingMessages.remove(localMessage.id);
|
||||
await _database.deleteMessage(localMessage.id);
|
||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||
await _database.saveMessageWithSender(updatedMessage);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
if (editingTo != null) {
|
||||
@@ -635,7 +695,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
_pendingMessages.remove(pendingMessageId);
|
||||
await _database.deleteMessage(pendingMessageId);
|
||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||
await _database.saveMessageWithSender(updatedMessage);
|
||||
|
||||
final newMessages =
|
||||
(state.value ?? []).map((m) {
|
||||
@@ -692,7 +752,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
await _database.saveMessageWithSender(localMessage);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final existingIndex = currentMessages.indexWhere(
|
||||
@@ -789,7 +849,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
messageToUpdate.status,
|
||||
);
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(deletedMessage));
|
||||
await _database.saveMessageWithSender(deletedMessage);
|
||||
|
||||
if (messageIndex != -1) {
|
||||
final newList = [...currentMessages];
|
||||
@@ -913,6 +973,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_searchQuery = null;
|
||||
_withLinks = null;
|
||||
_withAttachments = null;
|
||||
_allRemoteMessagesFetched = false;
|
||||
loadInitial();
|
||||
}
|
||||
|
||||
@@ -938,7 +999,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
await _database.saveMessageWithSender(message);
|
||||
return message;
|
||||
} catch (e) {
|
||||
if (e is DioException) return null;
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'27e5d686d9204ba39adbd1838cf4a6eaea0ac85f';
|
||||
String _$messagesNotifierHash() => r'27ce32c54e317a04e1d554ed4a70a24e4503fdd1';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -6,11 +6,12 @@ import 'package:island/widgets/content/sheet.dart';
|
||||
|
||||
class CaptchaScreen extends ConsumerWidget {
|
||||
static Future<String?> show(BuildContext context) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
return Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// ignore_for_file: invalid_runtime_check_with_js_interop_types
|
||||
|
||||
import 'dart:ui_web' as ui;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
@@ -10,11 +8,12 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class CaptchaScreen extends ConsumerStatefulWidget {
|
||||
static Future<String?> show(BuildContext context) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
return Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +28,9 @@ class _CaptchaScreenState extends ConsumerState<CaptchaScreen> {
|
||||
|
||||
void _setupWebListener(String serverUrl) async {
|
||||
web.window.onMessage.listen((event) {
|
||||
// ignore: invalid_runtime_check_with_js_interop_types
|
||||
if (event.data != null && event.data is String) {
|
||||
// ignore: invalid_runtime_check_with_js_interop_types
|
||||
final message = event.data as String;
|
||||
if (message.startsWith("captcha_tk=")) {
|
||||
String token = message.replaceFirst("captcha_tk=", "");
|
||||
|
||||
@@ -36,7 +36,7 @@ class FileListScreen extends HookConsumerWidget {
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text('files').tr(),
|
||||
leading: const PageBackButton(),
|
||||
leading: const PageBackButton(backTo: '/account'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.bar_chart),
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
@@ -68,6 +69,16 @@ class NotificationUnreadCountNotifier
|
||||
void clear() async {
|
||||
state = AsyncData(0);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/ring/notifications/count');
|
||||
state = AsyncData((response.data as num).toInt());
|
||||
} catch (_) {
|
||||
// Keep the current state if refresh fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
@@ -115,8 +126,36 @@ class NotificationListNotifier extends _$NotificationListNotifier
|
||||
class NotificationSheet extends HookConsumerWidget {
|
||||
const NotificationSheet({super.key});
|
||||
|
||||
IconData _getNotificationIcon(String topic) {
|
||||
switch (topic) {
|
||||
case 'post.replies':
|
||||
return Symbols.reply;
|
||||
case 'wallet.transactions':
|
||||
return Symbols.account_balance_wallet;
|
||||
case 'relationships.friends.request':
|
||||
return Symbols.person_add;
|
||||
case 'invites.chat':
|
||||
return Symbols.chat;
|
||||
case 'invites.realm':
|
||||
return Symbols.domain;
|
||||
case 'auth.login':
|
||||
return Symbols.login;
|
||||
case 'posts.new':
|
||||
return Symbols.post_add;
|
||||
case 'wallet.orders.paid':
|
||||
return Symbols.shopping_bag;
|
||||
case 'posts.reactions.new':
|
||||
return Symbols.add_reaction;
|
||||
default:
|
||||
return Symbols.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Refresh unread count when sheet opens to sync across devices
|
||||
ref.read(notificationUnreadCountNotifierProvider.notifier).refresh();
|
||||
|
||||
Future<void> markAllRead() async {
|
||||
showLoadingModal(context);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
@@ -149,12 +188,30 @@ class NotificationSheet extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
final notification = data.items[index];
|
||||
final pfp = notification.meta['pfp'] as String?;
|
||||
final images = notification.meta['images'] as List?;
|
||||
final imageIds = images?.cast<String>() ?? [];
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
leading:
|
||||
pfp != null
|
||||
? ProfilePictureWidget(fileId: pfp, radius: 20)
|
||||
: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Icon(
|
||||
_getNotificationIcon(notification.topic),
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
title: Text(notification.title),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -187,6 +244,29 @@ class NotificationSheet extends HookConsumerWidget {
|
||||
).colorScheme.onSurface.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
if (imageIds.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children:
|
||||
imageIds.map((imageId) {
|
||||
return SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CloudImageWidget(
|
||||
fileId: imageId,
|
||||
aspectRatio: 1,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing:
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:island/widgets/navigation/fab_menu.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/chat/chat_summary.dart';
|
||||
|
||||
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
||||
|
||||
@@ -50,6 +51,8 @@ class TabsScreen extends HookConsumerWidget {
|
||||
notificationUnreadCountNotifierProvider,
|
||||
);
|
||||
|
||||
final chatUnreadCount = ref.watch(chatUnreadCountNotifierProvider);
|
||||
|
||||
final wideScreen = isWideScreen(context);
|
||||
|
||||
final destinations = [
|
||||
@@ -59,7 +62,11 @@ class TabsScreen extends HookConsumerWidget {
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'chat'.tr(),
|
||||
icon: const Icon(Symbols.forum_rounded),
|
||||
icon: Badge.count(
|
||||
count: chatUnreadCount.value ?? 0,
|
||||
isLabelVisible: (chatUnreadCount.value ?? 0) > 0,
|
||||
child: const Icon(Symbols.forum_rounded),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'realms'.tr(),
|
||||
|
||||
@@ -25,12 +25,24 @@ const Map<String, Color> kUsernamePlainColors = {
|
||||
'white': Colors.white,
|
||||
};
|
||||
|
||||
const kVerificationMarkColors = [
|
||||
const List<IconData> kVerificationMarkIcons = [
|
||||
Symbols.build_circle,
|
||||
Symbols.verified,
|
||||
Symbols.verified,
|
||||
Symbols.account_balance,
|
||||
Symbols.palette,
|
||||
Symbols.code,
|
||||
Symbols.masks,
|
||||
];
|
||||
|
||||
const List<Color> kVerificationMarkColors = [
|
||||
Colors.teal,
|
||||
Colors.blue,
|
||||
Colors.amber,
|
||||
Colors.blueGrey,
|
||||
Colors.lightBlue,
|
||||
Colors.indigo,
|
||||
Colors.red,
|
||||
Colors.orange,
|
||||
Colors.blue,
|
||||
Colors.blueAccent,
|
||||
];
|
||||
|
||||
class AccountName extends StatelessWidget {
|
||||
@@ -291,13 +303,14 @@ class VerificationMark extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = Icon(
|
||||
mark.type == 4
|
||||
? Symbols.play_circle
|
||||
: mark.type == 0
|
||||
? Symbols.build_circle
|
||||
(kVerificationMarkIcons.length > mark.type && mark.type >= 0)
|
||||
? kVerificationMarkIcons[mark.type]
|
||||
: Symbols.verified,
|
||||
size: 16,
|
||||
color: kVerificationMarkColors[mark.type],
|
||||
color:
|
||||
(kVerificationMarkColors.length > mark.type && mark.type >= 0)
|
||||
? kVerificationMarkColors[mark.type]
|
||||
: Colors.blue,
|
||||
fill: 1,
|
||||
);
|
||||
|
||||
@@ -394,13 +407,14 @@ class VerificationStatusCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Icon(
|
||||
mark.type == 4
|
||||
? Symbols.play_circle
|
||||
: mark.type == 0
|
||||
? Symbols.build_circle
|
||||
(kVerificationMarkIcons.length > mark.type && mark.type >= 0)
|
||||
? kVerificationMarkIcons[mark.type]
|
||||
: Symbols.verified,
|
||||
size: 32,
|
||||
color: kVerificationMarkColors[mark.type],
|
||||
color:
|
||||
(kVerificationMarkColors.length > mark.type && mark.type >= 0)
|
||||
? kVerificationMarkColors[mark.type]
|
||||
: Colors.blue,
|
||||
fill: 1,
|
||||
).alignment(Alignment.centerLeft),
|
||||
const Gap(8),
|
||||
|
||||
@@ -76,8 +76,6 @@ class MessageSenderInfo extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountName(
|
||||
account: sender.account,
|
||||
@@ -86,19 +84,6 @@ class MessageSenderInfo extends StatelessWidget {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Badge(
|
||||
label:
|
||||
Text(
|
||||
sender.role >= 100
|
||||
? 'permissionOwner'
|
||||
: sender.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
timestamp,
|
||||
style: TextStyle(
|
||||
|
||||
@@ -55,16 +55,20 @@ class _EmbedLinkWidgetState extends State<EmbedLinkWidget> {
|
||||
stream.removeListener(listener);
|
||||
|
||||
final aspectRatio = info.image.width / info.image.height;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSquare = aspectRatio >= 0.9 && aspectRatio <= 1.1;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// If error, assume not square
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSquare = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _launchUrl() async {
|
||||
final uri = Uri.parse(widget.link.url);
|
||||
|
||||
@@ -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
|
||||
# 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.
|
||||
version: 3.3.0+147
|
||||
version: 3.3.0+148
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
|
||||
Reference in New Issue
Block a user