Compare commits
15 Commits
3.2.0+125
...
6892afb974
Author | SHA1 | Date | |
---|---|---|---|
|
6892afb974 | ||
|
007b46b080 | ||
|
67d130dc34 | ||
|
7e923c77fe | ||
|
a593b52812 | ||
|
520dc80303 | ||
|
001897bbcd | ||
|
bab29c23e3 | ||
|
76b39f2df3 | ||
|
509b3e145b | ||
|
2b80ebc2d0 | ||
|
0ab908dd2a | ||
|
6007467e7a | ||
|
3745157c42 | ||
|
94481ec7bd |
@@ -334,6 +334,7 @@
|
|||||||
"walletCreate": "Create a Wallet",
|
"walletCreate": "Create a Wallet",
|
||||||
"settingsServerUrl": "Server URL",
|
"settingsServerUrl": "Server URL",
|
||||||
"settingsApplied": "The settings has been applied.",
|
"settingsApplied": "The settings has been applied.",
|
||||||
|
"settingsCustomFontsHelper": "Use comma to seprate.",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"posts": "Posts",
|
"posts": "Posts",
|
||||||
"settingsBackgroundImage": "Background Image",
|
"settingsBackgroundImage": "Background Image",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -245,7 +245,7 @@ PODS:
|
|||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
- receive_sharing_intent (1.8.1):
|
- receive_sharing_intent (1.8.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- record_ios (1.0.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- SDWebImage (5.21.1):
|
- SDWebImage (5.21.1):
|
||||||
@@ -510,7 +510,7 @@ SPEC CHECKSUMS:
|
|||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
|
@@ -1,484 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:island/database/drift_db.dart';
|
|
||||||
import 'package:island/database/message.dart';
|
|
||||||
import 'package:island/models/chat.dart';
|
|
||||||
import 'package:island/models/file.dart';
|
|
||||||
import 'package:island/services/file.dart';
|
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
class MessageRepository {
|
|
||||||
final SnChatRoom room;
|
|
||||||
final SnChatMember identity;
|
|
||||||
final Dio _apiClient;
|
|
||||||
final AppDatabase _database;
|
|
||||||
|
|
||||||
final Map<String, LocalChatMessage> pendingMessages = {};
|
|
||||||
final Map<String, Map<int, double>> fileUploadProgress = {};
|
|
||||||
int? _totalCount;
|
|
||||||
|
|
||||||
MessageRepository(this.room, this.identity, this._apiClient, this._database);
|
|
||||||
|
|
||||||
Future<LocalChatMessage?> getLastMessages() async {
|
|
||||||
final dbMessages = await _database.getMessagesForRoom(
|
|
||||||
room.id,
|
|
||||||
offset: 0,
|
|
||||||
limit: 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dbMessages.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _database.companionToMessage(dbMessages.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> syncMessages() async {
|
|
||||||
final lastMessage = await getLastMessages();
|
|
||||||
if (lastMessage == null) return false;
|
|
||||||
try {
|
|
||||||
final resp = await _apiClient.post(
|
|
||||||
'/sphere/chat/${room.id}/sync',
|
|
||||||
data: {
|
|
||||||
'last_sync_timestamp':
|
|
||||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = MessageSyncResponse.fromJson(resp.data);
|
|
||||||
for (final change in response.changes) {
|
|
||||||
switch (change.action) {
|
|
||||||
case MessageChangeAction.create:
|
|
||||||
await receiveMessage(change.message!);
|
|
||||||
break;
|
|
||||||
case MessageChangeAction.update:
|
|
||||||
await receiveMessageUpdate(change.message!);
|
|
||||||
break;
|
|
||||||
case MessageChangeAction.delete:
|
|
||||||
await receiveMessageDeletion(change.messageId.toString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> listMessages({
|
|
||||||
int offset = 0,
|
|
||||||
int take = 20,
|
|
||||||
bool synced = false,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
// For initial load, fetch latest messages in the background to sync.
|
|
||||||
if (offset == 0 && !synced) {
|
|
||||||
// Not awaiting this is intentional, for a quicker UI response.
|
|
||||||
// The UI should rely on a stream from the database to get updates.
|
|
||||||
_fetchAndCacheMessages(room.id, offset: 0, take: take).catchError((_) {
|
|
||||||
// Best effort, errors will be handled by later fetches.
|
|
||||||
return <LocalChatMessage>[];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final localMessages = await _getCachedMessages(
|
|
||||||
room.id,
|
|
||||||
offset: offset,
|
|
||||||
take: take,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If local cache has messages, return them. This is the common case for scrolling up.
|
|
||||||
if (localMessages.isNotEmpty) {
|
|
||||||
return localMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If local cache is empty, we've probably reached the end of cached history.
|
|
||||||
// Fetch from remote. This will also be hit on first load if cache is empty.
|
|
||||||
return await _fetchAndCacheMessages(room.id, offset: offset, take: take);
|
|
||||||
} catch (e) {
|
|
||||||
// Final fallback to cache in case of network errors during fetch.
|
|
||||||
final localMessages = await _getCachedMessages(
|
|
||||||
room.id,
|
|
||||||
offset: offset,
|
|
||||||
take: take,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (localMessages.isNotEmpty) {
|
|
||||||
return localMessages;
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _getCachedMessages(
|
|
||||||
String roomId, {
|
|
||||||
int offset = 0,
|
|
||||||
int take = 20,
|
|
||||||
}) async {
|
|
||||||
// Get messages from local database
|
|
||||||
final dbMessages = await _database.getMessagesForRoom(
|
|
||||||
roomId,
|
|
||||||
offset: offset,
|
|
||||||
limit: take,
|
|
||||||
);
|
|
||||||
final dbLocalMessages =
|
|
||||||
dbMessages.map(_database.companionToMessage).toList();
|
|
||||||
|
|
||||||
// Combine with pending messages for the first page
|
|
||||||
if (offset == 0) {
|
|
||||||
final pendingForRoom =
|
|
||||||
pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
|
|
||||||
|
|
||||||
final allMessages = [...pendingForRoom, ...dbLocalMessages];
|
|
||||||
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
||||||
|
|
||||||
// Remove duplicates by ID, preserving the order
|
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
|
||||||
final seenIds = <String>{};
|
|
||||||
for (final message in allMessages) {
|
|
||||||
if (seenIds.add(message.id)) {
|
|
||||||
uniqueMessages.add(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uniqueMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbLocalMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _fetchAndCacheMessages(
|
|
||||||
String roomId, {
|
|
||||||
int offset = 0,
|
|
||||||
int take = 20,
|
|
||||||
}) async {
|
|
||||||
// Use cached total count if available, otherwise fetch it
|
|
||||||
if (_totalCount == null) {
|
|
||||||
final response = await _apiClient.get(
|
|
||||||
'/sphere/chat/$roomId/messages',
|
|
||||||
queryParameters: {'offset': 0, 'take': 1},
|
|
||||||
);
|
|
||||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset >= _totalCount!) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await _apiClient.get(
|
|
||||||
'/sphere/chat/$roomId/messages',
|
|
||||||
queryParameters: {'offset': offset, 'take': take},
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<dynamic> data = response.data;
|
|
||||||
// Update total count from response headers
|
|
||||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
|
||||||
|
|
||||||
final messages =
|
|
||||||
data.map((json) {
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(json);
|
|
||||||
return LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
for (final message in messages) {
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
|
||||||
if (message.nonce != null) {
|
|
||||||
pendingMessages.removeWhere(
|
|
||||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> sendMessage(
|
|
||||||
String token,
|
|
||||||
String baseUrl,
|
|
||||||
String roomId,
|
|
||||||
String content,
|
|
||||||
String nonce, {
|
|
||||||
required List<UniversalFile> attachments,
|
|
||||||
Map<String, dynamic>? meta,
|
|
||||||
SnChatMessage? replyingTo,
|
|
||||||
SnChatMessage? forwardingTo,
|
|
||||||
SnChatMessage? editingTo,
|
|
||||||
Function(LocalChatMessage)? onPending,
|
|
||||||
Function(String, Map<int, double>)? onProgress,
|
|
||||||
}) async {
|
|
||||||
// Generate a unique nonce for this message
|
|
||||||
final nonce = const Uuid().v4();
|
|
||||||
|
|
||||||
// Create a local message with pending status
|
|
||||||
final mockMessage = SnChatMessage(
|
|
||||||
id: 'pending_$nonce',
|
|
||||||
chatRoomId: roomId,
|
|
||||||
senderId: identity.id,
|
|
||||||
content: content,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
nonce: nonce,
|
|
||||||
sender: identity,
|
|
||||||
);
|
|
||||||
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
mockMessage,
|
|
||||||
MessageStatus.pending,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store in memory and database
|
|
||||||
pendingMessages[localMessage.id] = localMessage;
|
|
||||||
fileUploadProgress[localMessage.id] = {};
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
|
||||||
onPending?.call(localMessage);
|
|
||||||
|
|
||||||
try {
|
|
||||||
var cloudAttachments = List.empty(growable: true);
|
|
||||||
// Upload files
|
|
||||||
for (var idx = 0; idx < attachments.length; idx++) {
|
|
||||||
final cloudFile =
|
|
||||||
await putMediaToCloud(
|
|
||||||
fileData: attachments[idx],
|
|
||||||
atk: token,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: attachments[idx].data.name ?? 'Post media',
|
|
||||||
mimetype:
|
|
||||||
attachments[idx].data.mimeType ??
|
|
||||||
switch (attachments[idx].type) {
|
|
||||||
UniversalFileType.image => 'image/unknown',
|
|
||||||
UniversalFileType.video => 'video/unknown',
|
|
||||||
UniversalFileType.audio => 'audio/unknown',
|
|
||||||
UniversalFileType.file => 'application/octet-stream',
|
|
||||||
},
|
|
||||||
onProgress: (progress, _) {
|
|
||||||
fileUploadProgress[localMessage.id]?[idx] = progress;
|
|
||||||
onProgress?.call(
|
|
||||||
localMessage.id,
|
|
||||||
fileUploadProgress[localMessage.id] ?? {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).future;
|
|
||||||
if (cloudFile == null) {
|
|
||||||
throw ArgumentError('Failed to upload the file...');
|
|
||||||
}
|
|
||||||
cloudAttachments.add(cloudFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send to server
|
|
||||||
final response = await _apiClient.request(
|
|
||||||
editingTo == null
|
|
||||||
? '/sphere/chat/$roomId/messages'
|
|
||||||
: '/sphere/chat/$roomId/messages/${editingTo.id}',
|
|
||||||
data: {
|
|
||||||
'content': content,
|
|
||||||
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
|
|
||||||
'replied_message_id': replyingTo?.id,
|
|
||||||
'forwarded_message_id': forwardingTo?.id,
|
|
||||||
'meta': meta,
|
|
||||||
'nonce': nonce,
|
|
||||||
},
|
|
||||||
options: Options(method: editingTo == null ? 'POST' : 'PATCH'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update with server response
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove from pending and update in database
|
|
||||||
pendingMessages.remove(localMessage.id);
|
|
||||||
await _database.deleteMessage(localMessage.id);
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
|
||||||
|
|
||||||
return updatedMessage;
|
|
||||||
} catch (e) {
|
|
||||||
// Update status to failed
|
|
||||||
localMessage.status = MessageStatus.failed;
|
|
||||||
pendingMessages[localMessage.id] = localMessage;
|
|
||||||
await _database.updateMessageStatus(
|
|
||||||
localMessage.id,
|
|
||||||
MessageStatus.failed,
|
|
||||||
);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> retryMessage(String pendingMessageId) async {
|
|
||||||
final message = await getMessageById(pendingMessageId);
|
|
||||||
if (message == null) {
|
|
||||||
throw Exception('Message not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status back to pending
|
|
||||||
message.status = MessageStatus.pending;
|
|
||||||
pendingMessages[pendingMessageId] = message;
|
|
||||||
await _database.updateMessageStatus(
|
|
||||||
pendingMessageId,
|
|
||||||
MessageStatus.pending,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send to server
|
|
||||||
var remoteMessage = message.toRemoteMessage();
|
|
||||||
final response = await _apiClient.post(
|
|
||||||
'/sphere/chat/${message.roomId}/messages',
|
|
||||||
data: {
|
|
||||||
'content': remoteMessage.content,
|
|
||||||
'attachments_id': remoteMessage.attachments,
|
|
||||||
'meta': remoteMessage.meta,
|
|
||||||
'nonce': message.nonce,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update with server response
|
|
||||||
remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove from pending and update in database
|
|
||||||
pendingMessages.remove(pendingMessageId);
|
|
||||||
await _database.deleteMessage(pendingMessageId);
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
|
||||||
|
|
||||||
return updatedMessage;
|
|
||||||
} catch (e) {
|
|
||||||
// Update status to failed
|
|
||||||
message.status = MessageStatus.failed;
|
|
||||||
pendingMessages[pendingMessageId] = message;
|
|
||||||
await _database.updateMessageStatus(
|
|
||||||
pendingMessageId,
|
|
||||||
MessageStatus.failed,
|
|
||||||
);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> receiveMessage(SnChatMessage remoteMessage) async {
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remoteMessage.nonce != null) {
|
|
||||||
pendingMessages.removeWhere(
|
|
||||||
(_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
|
||||||
return localMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> receiveMessageUpdate(
|
|
||||||
SnChatMessage remoteMessage,
|
|
||||||
) async {
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
await _database.updateMessage(_database.messageToCompanion(localMessage));
|
|
||||||
return localMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> receiveMessageDeletion(String messageId) async {
|
|
||||||
// Remove from pending messages if exists
|
|
||||||
pendingMessages.remove(messageId);
|
|
||||||
|
|
||||||
// Delete from local database
|
|
||||||
await _database.deleteMessage(messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> updateMessage(
|
|
||||||
String messageId,
|
|
||||||
String content, {
|
|
||||||
List<SnCloudFile>? attachments,
|
|
||||||
Map<String, dynamic>? meta,
|
|
||||||
}) async {
|
|
||||||
final message = pendingMessages[messageId];
|
|
||||||
if (message != null) {
|
|
||||||
// Update pending message
|
|
||||||
final rmMessage = message.toRemoteMessage();
|
|
||||||
final updatedRemoteMessage = rmMessage.copyWith(
|
|
||||||
content: content,
|
|
||||||
meta: meta ?? rmMessage.meta,
|
|
||||||
);
|
|
||||||
final updatedLocalMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
updatedRemoteMessage,
|
|
||||||
MessageStatus.pending,
|
|
||||||
);
|
|
||||||
pendingMessages[messageId] = updatedLocalMessage;
|
|
||||||
await _database.updateMessage(
|
|
||||||
_database.messageToCompanion(updatedLocalMessage),
|
|
||||||
);
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Update on server
|
|
||||||
final response = await _apiClient.put(
|
|
||||||
'/sphere/chat/${room.id}/messages/$messageId',
|
|
||||||
data: {'content': content, 'attachments': attachments, 'meta': meta},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update local copy
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
await _database.updateMessage(
|
|
||||||
_database.messageToCompanion(updatedMessage),
|
|
||||||
);
|
|
||||||
return updatedMessage;
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteMessage(String messageId) async {
|
|
||||||
try {
|
|
||||||
await _apiClient.delete('/sphere/chat/${room.id}/messages/$messageId');
|
|
||||||
pendingMessages.remove(messageId);
|
|
||||||
await _database.deleteMessage(messageId);
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage?> getMessageById(String messageId) async {
|
|
||||||
try {
|
|
||||||
// Attempt to get the message from the local database
|
|
||||||
final localMessage =
|
|
||||||
await (_database.select(_database.chatMessages)
|
|
||||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
|
||||||
if (localMessage != null) {
|
|
||||||
return _database.companionToMessage(localMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found locally, fetch from the server
|
|
||||||
final response = await _apiClient.get(
|
|
||||||
'/sphere/chat/${room.id}/messages/$messageId',
|
|
||||||
);
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final message = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save the fetched message to the local database
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
|
||||||
return message;
|
|
||||||
} catch (e) {
|
|
||||||
if (e is DioException) return null;
|
|
||||||
// Handle errors
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -11,8 +11,8 @@ sealed class SnScrappedLink with _$SnScrappedLink {
|
|||||||
required String title,
|
required String title,
|
||||||
required String? description,
|
required String? description,
|
||||||
required String? imageUrl,
|
required String? imageUrl,
|
||||||
required String faviconUrl,
|
required String? faviconUrl,
|
||||||
required String siteName,
|
required String? siteName,
|
||||||
required String? contentType,
|
required String? contentType,
|
||||||
required String? author,
|
required String? author,
|
||||||
required DateTime? publishedDate,
|
required DateTime? publishedDate,
|
||||||
|
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnScrappedLink {
|
mixin _$SnScrappedLink {
|
||||||
|
|
||||||
String get type; String get url; String get title; String? get description; String? get imageUrl; String get faviconUrl; String get siteName; String? get contentType; String? get author; DateTime? get publishedDate;
|
String get type; String get url; String get title; String? get description; String? get imageUrl; String? get faviconUrl; String? get siteName; String? get contentType; String? get author; DateTime? get publishedDate;
|
||||||
/// Create a copy of SnScrappedLink
|
/// Create a copy of SnScrappedLink
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -48,7 +48,7 @@ abstract mixin class $SnScrappedLinkCopyWith<$Res> {
|
|||||||
factory $SnScrappedLinkCopyWith(SnScrappedLink value, $Res Function(SnScrappedLink) _then) = _$SnScrappedLinkCopyWithImpl;
|
factory $SnScrappedLinkCopyWith(SnScrappedLink value, $Res Function(SnScrappedLink) _then) = _$SnScrappedLinkCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate
|
String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -65,16 +65,16 @@ class _$SnScrappedLinkCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnScrappedLink
|
/// Create a copy of SnScrappedLink
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = freezed,Object? siteName = freezed,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
|
as String?,faviconUrl: freezed == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
|
as String?,siteName: freezed == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
|
||||||
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
as String?,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
|
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
@@ -159,7 +159,7 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnScrappedLink() when $default != null:
|
case _SnScrappedLink() when $default != null:
|
||||||
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
|
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
|
||||||
@@ -180,7 +180,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnScrappedLink():
|
case _SnScrappedLink():
|
||||||
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);}
|
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);}
|
||||||
@@ -197,7 +197,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnScrappedLink() when $default != null:
|
case _SnScrappedLink() when $default != null:
|
||||||
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
|
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
|
||||||
@@ -220,8 +220,8 @@ class _SnScrappedLink implements SnScrappedLink {
|
|||||||
@override final String title;
|
@override final String title;
|
||||||
@override final String? description;
|
@override final String? description;
|
||||||
@override final String? imageUrl;
|
@override final String? imageUrl;
|
||||||
@override final String faviconUrl;
|
@override final String? faviconUrl;
|
||||||
@override final String siteName;
|
@override final String? siteName;
|
||||||
@override final String? contentType;
|
@override final String? contentType;
|
||||||
@override final String? author;
|
@override final String? author;
|
||||||
@override final DateTime? publishedDate;
|
@override final DateTime? publishedDate;
|
||||||
@@ -259,7 +259,7 @@ abstract mixin class _$SnScrappedLinkCopyWith<$Res> implements $SnScrappedLinkCo
|
|||||||
factory _$SnScrappedLinkCopyWith(_SnScrappedLink value, $Res Function(_SnScrappedLink) _then) = __$SnScrappedLinkCopyWithImpl;
|
factory _$SnScrappedLinkCopyWith(_SnScrappedLink value, $Res Function(_SnScrappedLink) _then) = __$SnScrappedLinkCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate
|
String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -276,16 +276,16 @@ class __$SnScrappedLinkCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnScrappedLink
|
/// Create a copy of SnScrappedLink
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = freezed,Object? siteName = freezed,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
|
||||||
return _then(_SnScrappedLink(
|
return _then(_SnScrappedLink(
|
||||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
|
as String?,faviconUrl: freezed == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
|
as String?,siteName: freezed == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
|
||||||
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
as String?,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
|
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
|
@@ -13,8 +13,8 @@ _SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) =>
|
|||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
description: json['description'] as String?,
|
description: json['description'] as String?,
|
||||||
imageUrl: json['image_url'] as String?,
|
imageUrl: json['image_url'] as String?,
|
||||||
faviconUrl: json['favicon_url'] as String,
|
faviconUrl: json['favicon_url'] as String?,
|
||||||
siteName: json['site_name'] as String,
|
siteName: json['site_name'] as String?,
|
||||||
contentType: json['content_type'] as String?,
|
contentType: json['content_type'] as String?,
|
||||||
author: json['author'] as String?,
|
author: json['author'] as String?,
|
||||||
publishedDate:
|
publishedDate:
|
||||||
|
@@ -23,6 +23,8 @@ const kAppSoundEffects = 'app_sound_effects';
|
|||||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||||
const kAppWindowSize = 'app_window_size';
|
const kAppWindowSize = 'app_window_size';
|
||||||
const kAppEnterToSend = 'app_enter_to_send';
|
const kAppEnterToSend = 'app_enter_to_send';
|
||||||
|
const kFeaturedPostsCollapsedId =
|
||||||
|
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
|
import 'dart:io' show Platform;
|
||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
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';
|
||||||
import 'package:island/screens/about.dart';
|
import 'package:island/screens/about.dart';
|
||||||
@@ -56,13 +58,35 @@ final rootNavigatorKey = GlobalKey<NavigatorState>();
|
|||||||
final _shellNavigatorKey = GlobalKey<NavigatorState>();
|
final _shellNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final _tabsShellKey = GlobalKey<NavigatorState>();
|
final _tabsShellKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
Widget _tabPagesTransitionBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
return FadeThroughTransition(
|
||||||
|
animation: animation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
fillColor: Theme.of(context).colorScheme.surface,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _supportsAnalytics =>
|
||||||
|
kIsWeb ||
|
||||||
|
Platform.isAndroid ||
|
||||||
|
Platform.isIOS ||
|
||||||
|
Platform.isMacOS ||
|
||||||
|
Platform.isWindows;
|
||||||
|
|
||||||
// Provider for the router
|
// Provider for the router
|
||||||
final routerProvider = Provider<GoRouter>((ref) {
|
final routerProvider = Provider<GoRouter>((ref) {
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
navigatorKey: rootNavigatorKey,
|
navigatorKey: rootNavigatorKey,
|
||||||
initialLocation: '/',
|
initialLocation: '/',
|
||||||
observers: [
|
observers: [
|
||||||
FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance),
|
if (_supportsAnalytics)
|
||||||
|
FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance),
|
||||||
],
|
],
|
||||||
routes: [
|
routes: [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
@@ -339,7 +363,12 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'explore',
|
name: 'explore',
|
||||||
path: '/',
|
path: '/',
|
||||||
builder: (context, state) => const ExploreScreen(),
|
pageBuilder:
|
||||||
|
(context, state) => CustomTransitionPage(
|
||||||
|
key: const ValueKey('explore'),
|
||||||
|
child: const ExploreScreen(),
|
||||||
|
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
@@ -389,8 +418,12 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
|
|
||||||
// Chat tab
|
// Chat tab
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder:
|
pageBuilder:
|
||||||
(context, state, child) => ChatShellScreen(child: child),
|
(context, state, child) => CustomTransitionPage(
|
||||||
|
key: const ValueKey('chat'),
|
||||||
|
child: ChatShellScreen(child: child),
|
||||||
|
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'chatList',
|
name: 'chatList',
|
||||||
@@ -433,7 +466,12 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'realmList',
|
name: 'realmList',
|
||||||
path: '/realms',
|
path: '/realms',
|
||||||
builder: (context, state) => const RealmListScreen(),
|
pageBuilder:
|
||||||
|
(context, state) => CustomTransitionPage(
|
||||||
|
key: const ValueKey('realms'),
|
||||||
|
child: const RealmListScreen(),
|
||||||
|
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'realmNew',
|
name: 'realmNew',
|
||||||
@@ -461,8 +499,12 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
|
|
||||||
// Account tab
|
// Account tab
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder:
|
pageBuilder:
|
||||||
(context, state, child) => AccountShellScreen(child: child),
|
(context, state, child) => CustomTransitionPage(
|
||||||
|
key: const ValueKey('account'),
|
||||||
|
child: AccountShellScreen(child: child),
|
||||||
|
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'account',
|
name: 'account',
|
||||||
|
@@ -178,7 +178,8 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
|||||||
context,
|
context,
|
||||||
icon: Symbols.label,
|
icon: Symbols.label,
|
||||||
label: 'aboutDeviceName'.tr(),
|
label: 'aboutDeviceName'.tr(),
|
||||||
value: _deviceInfo?.data['name'],
|
value:
|
||||||
|
_deviceInfo?.data['name'] ?? 'unknown'.tr(),
|
||||||
),
|
),
|
||||||
_buildInfoItem(
|
_buildInfoItem(
|
||||||
context,
|
context,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ part of 'room.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'afc4d43f4948ec571118cef0321838a6cefc89c0';
|
String _$messagesNotifierHash() => r'3b10c3101404f6528c7a83baa0d39cba1a30f579';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@@ -11,6 +11,7 @@ import 'package:island/models/realm.dart';
|
|||||||
import 'package:island/models/webfeed.dart';
|
import 'package:island/models/webfeed.dart';
|
||||||
import 'package:island/pods/event_calendar.dart';
|
import 'package:island/pods/event_calendar.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/screens/notification.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/account/fortune_graph.dart';
|
import 'package:island/widgets/account/fortune_graph.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
@@ -30,6 +31,33 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
|
|
||||||
part 'explore.g.dart';
|
part 'explore.g.dart';
|
||||||
|
|
||||||
|
Widget notificationIndicatorWidget(
|
||||||
|
BuildContext context, {
|
||||||
|
required int count,
|
||||||
|
EdgeInsets? margin,
|
||||||
|
}) => Card(
|
||||||
|
margin: margin,
|
||||||
|
child: ListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
leading: const Icon(Symbols.notifications),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text('notifications').tr().fontSize(14),
|
||||||
|
const Gap(8),
|
||||||
|
Badge(label: Text(count.toString())),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
minTileHeight: 40,
|
||||||
|
contentPadding: EdgeInsets.only(left: 16, right: 15),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('notifications');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class ExploreScreen extends HookConsumerWidget {
|
class ExploreScreen extends HookConsumerWidget {
|
||||||
const ExploreScreen({super.key});
|
const ExploreScreen({super.key});
|
||||||
|
|
||||||
@@ -77,6 +105,10 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final user = ref.watch(userInfoProvider);
|
final user = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
|
final notificationCount = ref.watch(
|
||||||
|
notificationUnreadCountNotifierProvider,
|
||||||
|
);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -185,7 +217,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
floatingActionButtonLocation: TabbedFabLocation(context),
|
floatingActionButtonLocation: TabbedFabLocation(context),
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final isWider = isWiderScreen(context);
|
final isWide = isWideScreen(context);
|
||||||
|
|
||||||
final bodyView = _buildActivityList(
|
final bodyView = _buildActivityList(
|
||||||
context,
|
context,
|
||||||
@@ -193,40 +225,58 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
currentFilter.value,
|
currentFilter.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isWider) {
|
if (isWide) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(flex: 3, child: bodyView.padding(left: 8)),
|
Flexible(flex: 3, child: bodyView.padding(left: 8)),
|
||||||
if (user.value != null)
|
if (user.value != null)
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: SingleChildScrollView(
|
child: Align(
|
||||||
child: Column(
|
alignment: Alignment.topCenter,
|
||||||
children: [
|
child: SingleChildScrollView(
|
||||||
CheckInWidget(
|
child: Column(
|
||||||
margin: EdgeInsets.only(
|
children: [
|
||||||
|
CheckInWidget(
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 12,
|
||||||
|
top: 16,
|
||||||
|
),
|
||||||
|
onChecked: () {
|
||||||
|
ref.invalidate(
|
||||||
|
eventCalendarProvider(query.value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (notificationCount.value != null &&
|
||||||
|
notificationCount.value! > 0)
|
||||||
|
notificationIndicatorWidget(
|
||||||
|
context,
|
||||||
|
count: notificationCount.value ?? 0,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 12,
|
||||||
|
top: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PostFeaturedList().padding(
|
||||||
left: 8,
|
left: 8,
|
||||||
right: 12,
|
right: 12,
|
||||||
top: 16,
|
top: 8,
|
||||||
),
|
),
|
||||||
onChecked: () {
|
FortuneGraphWidget(
|
||||||
ref.invalidate(
|
margin: EdgeInsets.only(
|
||||||
eventCalendarProvider(query.value),
|
left: 8,
|
||||||
);
|
right: 12,
|
||||||
},
|
top: 8,
|
||||||
),
|
),
|
||||||
PostFeaturedList().padding(
|
events: events,
|
||||||
left: 8,
|
constrainWidth: true,
|
||||||
right: 12,
|
onPointSelected: onDaySelected,
|
||||||
top: 8,
|
),
|
||||||
),
|
],
|
||||||
FortuneGraphWidget(
|
),
|
||||||
margin: EdgeInsets.only(left: 8, right: 12, top: 8),
|
|
||||||
events: events,
|
|
||||||
constrainWidth: true,
|
|
||||||
onPointSelected: onDaySelected,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -268,7 +318,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
activityListNotifierProvider(filter).notifier,
|
activityListNotifierProvider(filter).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
final isWider = isWiderScreen(context);
|
final isWide = isWideScreen(context);
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
||||||
@@ -283,7 +333,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
widgetCount: widgetCount,
|
widgetCount: widgetCount,
|
||||||
endItemView: endItemView,
|
endItemView: endItemView,
|
||||||
activitiesNotifier: activitiesNotifier,
|
activitiesNotifier: activitiesNotifier,
|
||||||
contentOnly: isWider || filter != null,
|
contentOnly: isWide || filter != null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -380,6 +430,10 @@ class _ActivityListView extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final user = ref.watch(userInfoProvider);
|
final user = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
|
final notificationCount = ref.watch(
|
||||||
|
notificationUnreadCountNotifierProvider,
|
||||||
|
);
|
||||||
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverGap(12),
|
SliverGap(12),
|
||||||
@@ -393,6 +447,14 @@ class _ActivityListView extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4),
|
child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4),
|
||||||
),
|
),
|
||||||
|
if (!contentOnly)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: notificationIndicatorWidget(
|
||||||
|
context,
|
||||||
|
count: notificationCount.value ?? 0,
|
||||||
|
margin: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: widgetCount,
|
itemCount: widgetCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
@@ -3,14 +3,17 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.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';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.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';
|
||||||
@@ -62,6 +65,10 @@ class NotificationUnreadCountNotifier
|
|||||||
final current = await future;
|
final current = await future;
|
||||||
state = AsyncData(math.max(current - count, 0));
|
state = AsyncData(math.max(current - count, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() async {
|
||||||
|
state = AsyncData(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
@@ -111,8 +118,27 @@ class NotificationScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
Future<void> markAllRead() async {
|
||||||
|
showLoadingModal(context);
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
await apiClient.post('/pusher/notifications/all/read');
|
||||||
|
if (!context.mounted) return;
|
||||||
|
hideLoadingModal(context);
|
||||||
|
ref.invalidate(notificationListNotifierProvider);
|
||||||
|
ref.watch(notificationUnreadCountNotifierProvider.notifier).clear();
|
||||||
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: const Text('notifications').tr()),
|
appBar: AppBar(
|
||||||
|
title: const Text('notifications').tr(),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: markAllRead,
|
||||||
|
icon: const Icon(Symbols.mark_as_unread),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: PagingHelperView(
|
body: PagingHelperView(
|
||||||
provider: notificationListNotifierProvider,
|
provider: notificationListNotifierProvider,
|
||||||
futureRefreshable: notificationListNotifierProvider.future,
|
futureRefreshable: notificationListNotifierProvider.future,
|
||||||
|
@@ -7,7 +7,7 @@ part of 'notification.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$notificationUnreadCountNotifierHash() =>
|
String _$notificationUnreadCountNotifierHash() =>
|
||||||
r'd199abf0d16944587e747798399a267a790341f3';
|
r'0763b66bd64e5a9b7c317887e109ab367515dfa4';
|
||||||
|
|
||||||
/// See also [NotificationUnreadCountNotifier].
|
/// See also [NotificationUnreadCountNotifier].
|
||||||
@ProviderFor(NotificationUnreadCountNotifier)
|
@ProviderFor(NotificationUnreadCountNotifier)
|
||||||
|
@@ -51,12 +51,12 @@ class PostSearchNotifier
|
|||||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/sphere/posts/search',
|
'/sphere/posts',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'query': _currentQuery,
|
'query': _currentQuery,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'take': _pageSize,
|
'take': _pageSize,
|
||||||
'useVector': false,
|
'vector': false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -26,7 +26,12 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
|
|||||||
final notification = SnNotification.fromJson(pkt.data!);
|
final notification = SnNotification.fromJson(pkt.data!);
|
||||||
showTopSnackBar(
|
showTopSnackBar(
|
||||||
globalOverlay.currentState!,
|
globalOverlay.currentState!,
|
||||||
NotificationCard(notification: notification),
|
Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
|
child: NotificationCard(notification: notification),
|
||||||
|
),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (notification.meta['action_uri'] != null) {
|
if (notification.meta['action_uri'] != null) {
|
||||||
var uri = notification.meta['action_uri'] as String;
|
var uri = notification.meta['action_uri'] as String;
|
||||||
@@ -53,9 +58,9 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
|
|||||||
(Platform.isMacOS ||
|
(Platform.isMacOS ||
|
||||||
Platform.isWindows ||
|
Platform.isWindows ||
|
||||||
Platform.isLinux))
|
Platform.isLinux))
|
||||||
? 24
|
? 28
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
: MediaQuery.of(context).padding.top + 8,
|
: MediaQuery.of(context).padding.top + 16,
|
||||||
bottom: 16,
|
bottom: 16,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -162,7 +162,7 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
await apiClient.patch(
|
await apiClient.patch(
|
||||||
'/accounts/me/devices/$sessionId/label',
|
'/id/accounts/me/devices/$sessionId/label',
|
||||||
data: jsonEncode(label),
|
data: jsonEncode(label),
|
||||||
);
|
);
|
||||||
ref.invalidate(authDevicesProvider);
|
ref.invalidate(authDevicesProvider);
|
||||||
|
@@ -11,7 +11,12 @@ export 'content/alert.native.dart'
|
|||||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||||
showTopSnackBar(
|
showTopSnackBar(
|
||||||
globalOverlay.currentState!,
|
globalOverlay.currentState!,
|
||||||
Card(child: Text(message).padding(horizontal: 20, vertical: 16)),
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
|
child: Center(
|
||||||
|
child: Card(child: Text(message).padding(horizontal: 20, vertical: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
snackBarPosition: SnackBarPosition.bottom,
|
snackBarPosition: SnackBarPosition.bottom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -57,11 +57,11 @@ class EmbedLinkWidget extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// Favicon
|
// Favicon
|
||||||
if (link.faviconUrl.isNotEmpty) ...[
|
if (link.faviconUrl?.isNotEmpty ?? false) ...[
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
uri: link.faviconUrl,
|
uri: link.faviconUrl!,
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@@ -80,8 +80,8 @@ class EmbedLinkWidget extends StatelessWidget {
|
|||||||
// Site name
|
// Site name
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
link.siteName.isNotEmpty
|
(link.siteName?.isNotEmpty ?? false)
|
||||||
? link.siteName
|
? link.siteName!
|
||||||
: Uri.parse(link.url).host,
|
: Uri.parse(link.url).host,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
@@ -183,9 +183,15 @@ class MarkdownTextContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final content = ConstrainedBox(
|
final content = ClipRRect(
|
||||||
constraints: BoxConstraints(maxHeight: 360),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain),
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: 360),
|
||||||
|
child: UniversalImage(
|
||||||
|
uri: uri.toString(),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return content;
|
return content;
|
||||||
},
|
},
|
||||||
|
@@ -244,7 +244,6 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Categories field
|
// Categories field
|
||||||
// FIXME: Sometimes the entire dropdown crashes: 'package:flutter/src/rendering/stack.dart': Failed assertion: line 799 pos 12: 'firstChild == null || child != null': is not true.
|
|
||||||
DropdownButtonFormField2<SnPostCategory>(
|
DropdownButtonFormField2<SnPostCategory>(
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -306,7 +305,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
|||||||
value: currentCategories.isEmpty ? null : currentCategories.last,
|
value: currentCategories.isEmpty ? null : currentCategories.last,
|
||||||
onChanged: (_) {},
|
onChanged: (_) {},
|
||||||
selectedItemBuilder: (context) {
|
selectedItemBuilder: (context) {
|
||||||
return currentCategories.map((item) {
|
return (postCategories.value ?? []).map((item) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/widgets/post/post_item.dart';
|
import 'package:island/widgets/post/post_item.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:island/pods/config.dart'; // Import config.dart for shared preferences keys and provider
|
||||||
|
|
||||||
part 'post_featured.g.dart';
|
part 'post_featured.g.dart';
|
||||||
|
|
||||||
@@ -25,7 +26,13 @@ class PostFeaturedList extends HookConsumerWidget {
|
|||||||
final featuredPostsAsync = ref.watch(featuredPostsProvider);
|
final featuredPostsAsync = ref.watch(featuredPostsProvider);
|
||||||
|
|
||||||
final pageViewController = usePageController();
|
final pageViewController = usePageController();
|
||||||
|
final prefs = ref.watch(sharedPreferencesProvider);
|
||||||
final pageViewCurrent = useState(0);
|
final pageViewCurrent = useState(0);
|
||||||
|
final previousFirstPostId = useState<String?>(null);
|
||||||
|
final storedCollapsedId = useState<String?>(
|
||||||
|
prefs.getString(kFeaturedPostsCollapsedId),
|
||||||
|
);
|
||||||
|
final isCollapsed = useState(false);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
pageViewController.addListener(() {
|
pageViewController.addListener(() {
|
||||||
@@ -34,6 +41,59 @@ class PostFeaturedList extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [pageViewController]);
|
}, [pageViewController]);
|
||||||
|
|
||||||
|
// Log isCollapsed state changes
|
||||||
|
useEffect(() {
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}, [isCollapsed.value]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) {
|
||||||
|
final currentFirstPostId = featuredPostsAsync.value!.first.id;
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Current first post ID: $currentFirstPostId',
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Previous first post ID: ${previousFirstPostId.value}',
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Stored collapsed ID: ${storedCollapsedId.value}',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (previousFirstPostId.value == null) {
|
||||||
|
// Initial load
|
||||||
|
previousFirstPostId.value = currentFirstPostId;
|
||||||
|
isCollapsed.value = (storedCollapsedId.value == currentFirstPostId);
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Initial load. isCollapsed set to ${isCollapsed.value}',
|
||||||
|
);
|
||||||
|
} else if (previousFirstPostId.value != currentFirstPostId) {
|
||||||
|
// First post changed, expand by default
|
||||||
|
previousFirstPostId.value = currentFirstPostId;
|
||||||
|
isCollapsed.value = false;
|
||||||
|
prefs.remove(
|
||||||
|
kFeaturedPostsCollapsedId,
|
||||||
|
); // Clear stored ID if post changes
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: First post changed. isCollapsed set to false.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Same first post, maintain current collapse state
|
||||||
|
// No change needed for isCollapsed.value unless manually toggled
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Same first post. Maintaining current collapse state.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: featuredPostsAsync has no value or is empty.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [featuredPostsAsync.value]);
|
||||||
|
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -73,29 +133,69 @@ class PostFeaturedList extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
icon: const Icon(Symbols.arrow_right),
|
icon: const Icon(Symbols.arrow_right),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
isCollapsed.value = !isCollapsed.value;
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Manual toggle. isCollapsed set to ${isCollapsed.value}',
|
||||||
|
);
|
||||||
|
if (isCollapsed.value &&
|
||||||
|
featuredPostsAsync.hasValue &&
|
||||||
|
featuredPostsAsync.value!.isNotEmpty) {
|
||||||
|
prefs.setString(
|
||||||
|
kFeaturedPostsCollapsedId,
|
||||||
|
featuredPostsAsync.value!.first.id,
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Stored collapsed ID: ${featuredPostsAsync.value!.first.id}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
prefs.remove(kFeaturedPostsCollapsedId);
|
||||||
|
debugPrint(
|
||||||
|
'PostFeaturedList: Removed stored collapsed ID.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
isCollapsed.value
|
||||||
|
? Symbols.expand_more
|
||||||
|
: Symbols.expand_less,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16, vertical: 8),
|
).padding(horizontal: 16, vertical: 8),
|
||||||
featuredPostsAsync.when(
|
AnimatedSize(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
duration: const Duration(milliseconds: 300),
|
||||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
curve: Curves.easeInOut,
|
||||||
data: (posts) {
|
child: Visibility(
|
||||||
return SizedBox(
|
visible: !isCollapsed.value,
|
||||||
height: 320,
|
child: featuredPostsAsync.when(
|
||||||
child: PageView.builder(
|
loading:
|
||||||
controller: pageViewController,
|
() => const Center(child: CircularProgressIndicator()),
|
||||||
scrollDirection: Axis.horizontal,
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||||
itemCount: posts.length,
|
data: (posts) {
|
||||||
itemBuilder: (context, index) {
|
return SizedBox(
|
||||||
return SingleChildScrollView(
|
height: 320,
|
||||||
child: PostActionableItem(
|
child: PageView.builder(
|
||||||
item: posts[index],
|
controller: pageViewController,
|
||||||
borderRadius: 8,
|
scrollDirection: Axis.horizontal,
|
||||||
),
|
itemCount: posts.length,
|
||||||
);
|
itemBuilder: (context, index) {
|
||||||
},
|
return SingleChildScrollView(
|
||||||
),
|
child: PostActionableItem(
|
||||||
);
|
item: posts[index],
|
||||||
},
|
borderRadius: 8,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -879,7 +879,8 @@ class _LinkPreview extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Favicon and image
|
// Favicon and image
|
||||||
if (embed.imageUrl != null || embed.faviconUrl.isNotEmpty)
|
if (embed.imageUrl != null ||
|
||||||
|
(embed.faviconUrl?.isNotEmpty ?? false))
|
||||||
Container(
|
Container(
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
@@ -899,11 +900,14 @@ class _LinkPreview extends ConsumerWidget {
|
|||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return _buildFaviconFallback(
|
return _buildFaviconFallback(
|
||||||
context,
|
context,
|
||||||
embed.faviconUrl,
|
embed.faviconUrl ?? '',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: _buildFaviconFallback(context, embed.faviconUrl),
|
: _buildFaviconFallback(
|
||||||
|
context,
|
||||||
|
embed.faviconUrl ?? '',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Content
|
// Content
|
||||||
@@ -912,9 +916,9 @@ class _LinkPreview extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Site name
|
// Site name
|
||||||
if (embed.siteName.isNotEmpty)
|
if (embed.siteName?.isNotEmpty ?? false)
|
||||||
Text(
|
Text(
|
||||||
embed.siteName,
|
embed.siteName!,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
@@ -41,7 +41,7 @@ endif()
|
|||||||
# of modifying this function.
|
# of modifying this function.
|
||||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
target_compile_options(${TARGET} PRIVATE -Wall -Wextra)
|
||||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
@@ -195,7 +195,7 @@ PODS:
|
|||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PromisesSwift (2.4.0):
|
- PromisesSwift (2.4.0):
|
||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
- record_macos (1.0.0):
|
- record_macos (1.1.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
@@ -422,7 +422,7 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0
|
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
|
36
pubspec.lock
36
pubspec.lock
@@ -1281,10 +1281,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker_platform_interface
|
name: image_picker_platform_interface
|
||||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.1"
|
version: "2.11.0"
|
||||||
image_picker_windows:
|
image_picker_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1897,58 +1897,58 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: record
|
name: record
|
||||||
sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817
|
sha256: "3d08502b77edf2a864aa6e4cd7874b983d42a80f3689431da053cc5e85c1ad21"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.1.0"
|
||||||
record_android:
|
record_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_android
|
name: record_android
|
||||||
sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b"
|
sha256: "8b170e33d9866f9b51e01a767d7e1ecb97b9ecd629950bd87a47c79359ec57f8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.4.0"
|
||||||
record_ios:
|
record_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_ios
|
name: record_ios
|
||||||
sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb"
|
sha256: ad97d0a75933c44bcf5aff648e86e32fc05eb61f8fbef190f14968c8eaf86692
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.1.0"
|
||||||
record_linux:
|
record_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_linux
|
name: record_linux
|
||||||
sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95"
|
sha256: "785e8e8d6db109aa606d0669d95aaae416458aaa39782bb0abe0bee74eee17d7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.2.0"
|
||||||
record_macos:
|
record_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_macos
|
name: record_macos
|
||||||
sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd"
|
sha256: f1399bca76a1634da109e5b0cba764ed8332a2b4da49c704c66d2c553405ed81
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.1.0"
|
||||||
record_platform_interface:
|
record_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_platform_interface
|
name: record_platform_interface
|
||||||
sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89
|
sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.4.0"
|
||||||
record_web:
|
record_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_web
|
name: record_web
|
||||||
sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb
|
sha256: "4f0adf20c9ccafcc02d71111fd91fba1ca7b17a7453902593e5a9b25b74a5c56"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.9"
|
version: "1.2.0"
|
||||||
record_windows:
|
record_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2568,10 +2568,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics_compiler
|
name: vector_graphics_compiler
|
||||||
sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331"
|
sha256: ca81fdfaf62a5ab45d7296614aea108d2c7d0efca8393e96174bf4d51e6725b0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.17"
|
version: "1.1.18"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@@ -75,7 +75,7 @@ dependencies:
|
|||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
file_picker: ^10.3.1
|
file_picker: ^10.3.1
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
image_picker_platform_interface: ^2.10.1
|
image_picker_platform_interface: ^2.11.0
|
||||||
image_picker_android: ^0.8.12+25
|
image_picker_android: ^0.8.12+25
|
||||||
super_context_menu: ^0.9.1
|
super_context_menu: ^0.9.1
|
||||||
modal_bottom_sheet: ^3.0.0
|
modal_bottom_sheet: ^3.0.0
|
||||||
@@ -107,7 +107,7 @@ dependencies:
|
|||||||
livekit_client: ^2.5.0+hotfix.1
|
livekit_client: ^2.5.0+hotfix.1
|
||||||
pasteboard: ^0.4.0
|
pasteboard: ^0.4.0
|
||||||
flutter_colorpicker: ^1.1.0
|
flutter_colorpicker: ^1.1.0
|
||||||
record: ^6.0.0
|
record: ^6.1.0
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
flutter_otp_text_field: ^1.5.1+1
|
flutter_otp_text_field: ^1.5.1+1
|
||||||
palette_generator: ^0.3.3+7
|
palette_generator: ^0.3.3+7
|
||||||
|
Reference in New Issue
Block a user