Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
cb05ff2e9e | |||
f614da7918 | |||
a3c8dafff9 | |||
fa978a7cd1 | |||
aaa0a562b4 | |||
590a4ce2a6 | |||
f26edce071 | |||
603799ea32 | |||
a32baf7798 | |||
498c9af663 | |||
202dbff6d3 | |||
96fd64d85d | |||
e236b7f98b | |||
5c7929e618 | |||
7ba5260246 | |||
a6d4947a23 |
@ -281,7 +281,12 @@
|
|||||||
"one": "{} attachment",
|
"one": "{} attachment",
|
||||||
"other": "{} attachments"
|
"other": "{} attachments"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} is typing...",
|
||||||
|
"other": "{} are typing..."
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "Random ID",
|
"fieldAttachmentRandomId": "Random ID",
|
||||||
|
"fieldAttachmentAlt": "Alternative text",
|
||||||
"addAttachmentFromAlbum": "Add from album",
|
"addAttachmentFromAlbum": "Add from album",
|
||||||
"addAttachmentFromClipboard": "Paste file",
|
"addAttachmentFromClipboard": "Paste file",
|
||||||
"addAttachmentFromCameraPhoto": "Take photo",
|
"addAttachmentFromCameraPhoto": "Take photo",
|
||||||
@ -293,6 +298,7 @@
|
|||||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||||
"attachmentCompressVideo": "Re-encode video",
|
"attachmentCompressVideo": "Re-encode video",
|
||||||
"attachmentSetThumbnail": "Set thumbnail",
|
"attachmentSetThumbnail": "Set thumbnail",
|
||||||
|
"attachmentSetAlt": "Set alternative text",
|
||||||
"attachmentCopyRandomId": "Copy RID",
|
"attachmentCopyRandomId": "Copy RID",
|
||||||
"attachmentUpload": "Upload",
|
"attachmentUpload": "Upload",
|
||||||
"attachmentInputDialog": "Upload attachments",
|
"attachmentInputDialog": "Upload attachments",
|
||||||
@ -467,6 +473,7 @@
|
|||||||
"accountStatusLastSeen": "Last seen at {}",
|
"accountStatusLastSeen": "Last seen at {}",
|
||||||
"postArticle": "Article on the Solar Network",
|
"postArticle": "Article on the Solar Network",
|
||||||
"postStory": "Story on the Solar Network",
|
"postStory": "Story on the Solar Network",
|
||||||
|
"postLocalDraftRestored": "Restored from device",
|
||||||
"articleWrittenAt": "Written at {}",
|
"articleWrittenAt": "Written at {}",
|
||||||
"articleEditedAt": "Edited at {}",
|
"articleEditedAt": "Edited at {}",
|
||||||
"attachmentSaved": "Saved to album",
|
"attachmentSaved": "Saved to album",
|
||||||
|
@ -279,7 +279,12 @@
|
|||||||
"one": "{} 个附件",
|
"one": "{} 个附件",
|
||||||
"other": "{} 个附件"
|
"other": "{} 个附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在输入",
|
||||||
|
"other": "{} 正在输入"
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "访问 ID",
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘贴附件",
|
"addAttachmentFromClipboard": "粘贴附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||||
@ -291,6 +296,7 @@
|
|||||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||||
"attachmentCompressVideo": "重新编码视频",
|
"attachmentCompressVideo": "重新编码视频",
|
||||||
"attachmentSetThumbnail": "设置缩略图",
|
"attachmentSetThumbnail": "设置缩略图",
|
||||||
|
"attachmentSetAlt": "设置概述文字",
|
||||||
"attachmentCopyRandomId": "复制访问 ID",
|
"attachmentCopyRandomId": "复制访问 ID",
|
||||||
"attachmentUpload": "上传",
|
"attachmentUpload": "上传",
|
||||||
"attachmentInputDialog": "上传附件",
|
"attachmentInputDialog": "上传附件",
|
||||||
@ -465,6 +471,7 @@
|
|||||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "从本地恢复草稿",
|
||||||
"articleWrittenAt": "发表于 {}",
|
"articleWrittenAt": "发表于 {}",
|
||||||
"articleEditedAt": "编辑于 {}",
|
"articleEditedAt": "编辑于 {}",
|
||||||
"attachmentSaved": "已保存到相册",
|
"attachmentSaved": "已保存到相册",
|
||||||
|
@ -280,6 +280,7 @@
|
|||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
@ -291,6 +292,7 @@
|
|||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
"attachmentCompressVideo": "重新編碼視頻",
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
"attachmentCopyRandomId": "複製訪問 ID",
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
"attachmentInputDialog": "上傳附件",
|
"attachmentInputDialog": "上傳附件",
|
||||||
@ -456,6 +458,7 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
@ -464,6 +467,7 @@
|
|||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已保存到相冊",
|
"attachmentSaved": "已保存到相冊",
|
||||||
|
@ -280,6 +280,7 @@
|
|||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
@ -291,6 +292,7 @@
|
|||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
"attachmentCompressVideo": "重新編碼視頻",
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
"attachmentCopyRandomId": "複製訪問 ID",
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
"attachmentInputDialog": "上傳附件",
|
"attachmentInputDialog": "上傳附件",
|
||||||
@ -456,6 +458,7 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
@ -464,6 +467,7 @@
|
|||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已保存到相冊",
|
"attachmentSaved": "已保存到相冊",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -11,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
|
import 'package:surface/types/websocket.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatMessageController extends ChangeNotifier {
|
class ChatMessageController extends ChangeNotifier {
|
||||||
@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
int? messageTotal;
|
int? messageTotal;
|
||||||
|
|
||||||
bool get isAllLoaded =>
|
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
|
||||||
messageTotal != null && messages.length >= messageTotal!;
|
|
||||||
|
|
||||||
String? _boxKey;
|
String? _boxKey;
|
||||||
SnChannel? channel;
|
SnChannel? channel;
|
||||||
@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
/// Stored as a list of nonce to provide the loading state
|
/// Stored as a list of nonce to provide the loading state
|
||||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||||
|
|
||||||
Box<SnChatMessage>? get _box =>
|
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||||
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
|
||||||
|
final List<SnChannelMember> typingMembers = List.empty(growable: true);
|
||||||
|
final Map<int, Timer> typingInactiveTimer = {};
|
||||||
|
|
||||||
Future<void> initialize(SnChannel chan) async {
|
Future<void> initialize(SnChannel chan) async {
|
||||||
channel = chan;
|
channel = chan;
|
||||||
@ -78,22 +81,17 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
if (event.payload?['channel_id'] != channel?.id) break;
|
if (event.payload?['channel_id'] != channel?.id) break;
|
||||||
final member = SnChannelMember.fromJson(event.payload!['member']);
|
final member = SnChannelMember.fromJson(event.payload!['member']);
|
||||||
if (member.id == profile?.id) break;
|
if (member.id == profile?.id) break;
|
||||||
// TODO impl typing users
|
if (!typingMembers.any((x) => x.id == member.id)) {
|
||||||
// if (!_typingUsers.any((x) => x.id == member.id)) {
|
typingMembers.add(member);
|
||||||
// setState(() {
|
print('Typing member: ${typingMembers.map((ele) => member.id)}');
|
||||||
// _typingUsers.add(member);
|
notifyListeners();
|
||||||
// });
|
}
|
||||||
// }
|
typingInactiveTimer[member.id]?.cancel();
|
||||||
// _typingInactiveTimer[member.id]?.cancel();
|
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
|
||||||
// _typingInactiveTimer[member.id] = Timer(
|
typingMembers.removeWhere((x) => x.id == member.id);
|
||||||
// const Duration(seconds: 3),
|
typingInactiveTimer.remove(member.id);
|
||||||
// () {
|
notifyListeners();
|
||||||
// setState(() {
|
});
|
||||||
// _typingUsers.removeWhere((x) => x.id == member.id);
|
|
||||||
// _typingInactiveTimer.remove(member.id);
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer? _typingNotifyTimer;
|
||||||
|
bool _typingStatus = false;
|
||||||
|
|
||||||
|
Future<void> _sendTypingStatusPackage() async {
|
||||||
|
_ws.conn?.sink.add(jsonEncode(
|
||||||
|
WebSocketPackage(
|
||||||
|
method: 'status.typing',
|
||||||
|
endpoint: 'im',
|
||||||
|
payload: {
|
||||||
|
'channel_id': channel!.id,
|
||||||
|
},
|
||||||
|
).toJson(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void pingTypingStatus() {
|
||||||
|
if (!_typingStatus) {
|
||||||
|
_sendTypingStatusPackage();
|
||||||
|
_typingStatus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
|
||||||
|
_typingNotifyTimer?.cancel();
|
||||||
|
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
|
||||||
|
_typingStatus = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
||||||
if (_box == null) return;
|
if (_box == null) return;
|
||||||
await _box!.putAll({
|
await _box!.putAll({
|
||||||
@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'messages.edit':
|
case 'messages.edit':
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
final idx =
|
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||||
messages.indexWhere((x) => x.id == message.relatedEventId);
|
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
final newBody = message.body;
|
final newBody = message.body;
|
||||||
newBody.remove('related_event');
|
newBody.remove('related_event');
|
||||||
@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
'algorithm': 'plain',
|
'algorithm': 'plain',
|
||||||
if (quoteId != null) 'quote_event': quoteId,
|
if (quoteId != null) 'quote_event': quoteId,
|
||||||
if (relatedId != null) 'related_event': relatedId,
|
if (relatedId != null) 'related_event': relatedId,
|
||||||
if (attachments != null && attachments.isNotEmpty)
|
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
|
||||||
'attachments': attachments,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the message locally
|
// Mock the message locally
|
||||||
@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client
|
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||||
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
|
||||||
out = SnChatMessage.fromJson(resp.data);
|
out = SnChatMessage.fromJson(resp.data);
|
||||||
_saveMessageToLocal([out]);
|
_saveMessageToLocal([out]);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
bool forceRemote = false,
|
bool forceRemote = false,
|
||||||
}) async {
|
}) async {
|
||||||
late List<SnChatMessage> out;
|
late List<SnChatMessage> out;
|
||||||
if (_box != null &&
|
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
|
||||||
(_box!.length >= take + offset || forceLocal) &&
|
|
||||||
!forceRemote) {
|
|
||||||
out = _box!.keys
|
out = _box!.keys
|
||||||
.toList()
|
.toList()
|
||||||
.cast<int>()
|
.cast<int>()
|
||||||
@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
quoteEvent: quoteEvent,
|
quoteEvent: quoteEvent,
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.where(
|
.where(
|
||||||
(ele) =>
|
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||||
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preload sender accounts
|
// Preload sender accounts
|
||||||
final accountId = out
|
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
|
||||||
.where((ele) => ele.sender.accountId >= 0)
|
|
||||||
.map((ele) => ele.sender.accountId)
|
|
||||||
.toSet();
|
|
||||||
await _ud.listAccount(accountId);
|
await _ud.listAccount(accountId);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
@ -8,6 +11,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@ -100,7 +104,7 @@ class PostWriteMedia {
|
|||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||||
if (width != null && height != null) {
|
if (width != null && height != null && !kIsWeb) {
|
||||||
return ResizeImage(
|
return ResizeImage(
|
||||||
provider,
|
provider,
|
||||||
width: width,
|
width: width,
|
||||||
@ -151,8 +155,18 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
final TextEditingController aliasController = TextEditingController();
|
final TextEditingController aliasController = TextEditingController();
|
||||||
|
|
||||||
PostWriteController() {
|
PostWriteController() {
|
||||||
titleController.addListener(() => notifyListeners());
|
titleController.addListener(() {
|
||||||
descriptionController.addListener(() => notifyListeners());
|
_temporaryPlanSave();
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
descriptionController.addListener(() {
|
||||||
|
_temporaryPlanSave();
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
contentController.addListener(() {
|
||||||
|
_temporaryPlanSave();
|
||||||
|
});
|
||||||
|
_temporaryLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
String mode = kTitleMap.keys.first;
|
String mode = kTitleMap.keys.first;
|
||||||
@ -199,11 +213,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
aliasController.text = post.alias ?? '';
|
aliasController.text = post.alias ?? '';
|
||||||
publishedAt = post.publishedAt;
|
publishedAt = post.publishedAt;
|
||||||
publishedUntil = post.publishedUntil;
|
publishedUntil = post.publishedUntil;
|
||||||
visibleUsers = List.from(post.visibleUsersList ?? []);
|
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||||
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||||
visibility = post.visibility;
|
visibility = post.visibility;
|
||||||
tags = List.from(post.tags.map((ele) => ele.alias));
|
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||||
categories = List.from(post.categories.map((ele) => ele.alias));
|
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||||
|
|
||||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||||
@ -298,6 +312,82 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
return compressedAttachment;
|
return compressedAttachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const kTemporaryStorageKey = 'int_draft_post';
|
||||||
|
|
||||||
|
Timer? _temporarySaveTimer;
|
||||||
|
|
||||||
|
void _temporaryPlanSave() {
|
||||||
|
_temporarySaveTimer?.cancel();
|
||||||
|
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
|
||||||
|
_temporarySave();
|
||||||
|
log("[PostWriter] Temporary save saved.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _temporarySave() {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
if (titleController.text.isEmpty &&
|
||||||
|
descriptionController.text.isEmpty &&
|
||||||
|
contentController.text.isEmpty &&
|
||||||
|
thumbnail == null &&
|
||||||
|
attachments.isEmpty) {
|
||||||
|
prefs.remove(kTemporaryStorageKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.setString(
|
||||||
|
kTemporaryStorageKey,
|
||||||
|
jsonEncode({
|
||||||
|
'publisher': publisher,
|
||||||
|
'content': contentController.text,
|
||||||
|
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||||
|
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||||
|
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||||
|
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||||
|
'attachments':
|
||||||
|
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
||||||
|
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
|
||||||
|
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||||
|
'visibility': visibility,
|
||||||
|
'visible_users_list': visibleUsers,
|
||||||
|
'invisible_users_list': invisibleUsers,
|
||||||
|
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||||
|
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool temporaryRestored = false;
|
||||||
|
|
||||||
|
void _temporaryLoad() {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
final raw = prefs.getString(kTemporaryStorageKey);
|
||||||
|
if (raw == null) return;
|
||||||
|
final data = jsonDecode(raw);
|
||||||
|
contentController.text = data['content'];
|
||||||
|
aliasController.text = data['alias'] ?? '';
|
||||||
|
titleController.text = data['title'] ?? '';
|
||||||
|
descriptionController.text = data['description'] ?? '';
|
||||||
|
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||||
|
attachments
|
||||||
|
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||||
|
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||||
|
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||||
|
visibility = data['visibility'];
|
||||||
|
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||||
|
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||||
|
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||||
|
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||||
|
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||||
|
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||||
|
temporaryRestored = true;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||||
if (isBusy) return;
|
if (isBusy) return;
|
||||||
|
|
||||||
@ -354,10 +444,12 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (context.mounted) {
|
||||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||||
if (compressedAttachment != null) {
|
if (compressedAttachment != null) {
|
||||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (context.mounted) context.showErrorDialog(err);
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
}
|
}
|
||||||
@ -415,6 +507,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
method: editingPost != null ? 'PUT' : 'POST',
|
method: editingPost != null ? 'PUT' : 'POST',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
reset();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -463,56 +556,67 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
|
|
||||||
void setPublisher(SnPublisher? item) {
|
void setPublisher(SnPublisher? item) {
|
||||||
publisher = item;
|
publisher = item;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPublishedAt(DateTime? value) {
|
void setPublishedAt(DateTime? value) {
|
||||||
publishedAt = value;
|
publishedAt = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPublishedUntil(DateTime? value) {
|
void setPublishedUntil(DateTime? value) {
|
||||||
publishedUntil = value;
|
publishedUntil = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTags(List<String> value) {
|
void setTags(List<String> value) {
|
||||||
tags = value;
|
tags = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCategories(List<String> value) {
|
void setCategories(List<String> value) {
|
||||||
categories = value;
|
categories = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVisibility(int value) {
|
void setVisibility(int value) {
|
||||||
visibility = value;
|
visibility = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVisibleUsers(List<int> value) {
|
void setVisibleUsers(List<int> value) {
|
||||||
visibleUsers = value;
|
visibleUsers = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setInvisibleUsers(List<int> value) {
|
void setInvisibleUsers(List<int> value) {
|
||||||
invisibleUsers = value;
|
invisibleUsers = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setProgress(double? value) {
|
void setProgress(double? value) {
|
||||||
progress = value;
|
progress = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setIsBusy(bool value) {
|
void setIsBusy(bool value) {
|
||||||
isBusy = value;
|
isBusy = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMode(String value) {
|
void setMode(String value) {
|
||||||
mode = value;
|
mode = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -530,6 +634,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
replyingPost = null;
|
replyingPost = null;
|
||||||
repostingPost = null;
|
repostingPost = null;
|
||||||
mode = kTitleMap.keys.first;
|
mode = kTitleMap.keys.first;
|
||||||
|
temporaryRestored = false;
|
||||||
|
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
lib/providers/sticker.dart
Normal file
3
lib/providers/sticker.dart
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
class StickerProvider {
|
||||||
|
|
||||||
|
}
|
@ -87,7 +87,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
try {
|
try {
|
||||||
final resp = await sn.client.request(
|
final resp = await sn.client.request(
|
||||||
widget.editingChannelAlias != null
|
widget.editingChannelAlias != null
|
||||||
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
|
? '/cgi/im/channels/$scope/${_editingChannel!.id}'
|
||||||
: '/cgi/im/channels/$scope',
|
: '/cgi/im/channels/$scope',
|
||||||
data: payload,
|
data: payload,
|
||||||
options: Options(
|
options: Options(
|
||||||
|
@ -17,6 +17,7 @@ import 'package:surface/types/chat.dart';
|
|||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message.dart';
|
import 'package:surface/widgets/chat/chat_message.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||||
|
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
@ -280,11 +281,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(top: 12),
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
top: 12,
|
|
||||||
),
|
|
||||||
hasReachedMax: _messageController.isAllLoaded,
|
hasReachedMax: _messageController.isAllLoaded,
|
||||||
itemCount: _messageController.messages.length,
|
itemCount: _messageController.messages.length,
|
||||||
isLoading: _messageController.isLoading,
|
isLoading: _messageController.isLoading,
|
||||||
@ -310,8 +307,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 480),
|
|
||||||
child: ChatMessage(
|
child: ChatMessage(
|
||||||
data: message,
|
data: message,
|
||||||
isMerged: canMerge,
|
isMerged: canMerge,
|
||||||
@ -327,7 +322,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_inputGlobalKey.currentState?.deleteMessage(value);
|
_inputGlobalKey.currentState?.deleteMessage(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -335,11 +329,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
if (!_messageController.isPending)
|
if (!_messageController.isPending)
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: ChatMessageInput(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ChatTypingIndicator(controller: _messageController),
|
||||||
|
ChatMessageInput(
|
||||||
key: _inputGlobalKey,
|
key: _inputGlobalKey,
|
||||||
otherMember: _otherMember,
|
otherMember: _otherMember,
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -153,9 +153,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashSpecialDayWidget extends StatelessWidget {
|
class _HomeDashSpecialDayWidget extends StatefulWidget {
|
||||||
const _HomeDashSpecialDayWidget();
|
const _HomeDashSpecialDayWidget();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
@ -165,7 +170,6 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
|
|
||||||
if (days.isNotEmpty) {
|
if (days.isNotEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
spacing: 8,
|
|
||||||
children: days.map((ele) {
|
children: days.map((ele) {
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
@ -173,8 +177,8 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
||||||
month: kSpecialDays[ele]!.$1,
|
month: kSpecialDays[ele]?.$1,
|
||||||
day: kSpecialDays[ele]!.$2,
|
day: kSpecialDays[ele]?.$2,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -204,6 +208,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
separatorType: SeparatorType.symbol,
|
separatorType: SeparatorType.symbol,
|
||||||
decoration: BoxDecoration(),
|
decoration: BoxDecoration(),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
onDone: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -82,24 +82,15 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isSubmitting = true);
|
setState(() => _isSubmitting = true);
|
||||||
|
|
||||||
List<int> markList = List.empty(growable: true);
|
|
||||||
for (final element in _notifications) {
|
|
||||||
if (element.id <= 0) continue;
|
|
||||||
if (element.readAt != null) continue;
|
|
||||||
markList.add(element.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.put('/cgi/id/notifications/read', data: {
|
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
||||||
'messages': markList,
|
|
||||||
});
|
|
||||||
_notifications.clear();
|
_notifications.clear();
|
||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar(
|
||||||
'notificationMarkAllReadPrompt'.plural(markList.length),
|
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
@ -364,6 +364,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Container(
|
||||||
|
child: _writeController.temporaryRestored
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.restore, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(child: Text('postLocalDraftRestored').tr()),
|
||||||
|
InkWell(
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
onTap: () {
|
||||||
|
_writeController.reset();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
)
|
||||||
|
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||||
|
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
||||||
LoadingIndicator(isActive: _isLoading),
|
LoadingIndicator(isActive: _isLoading),
|
||||||
if (_writeController.isBusy && _writeController.progress != null)
|
if (_writeController.isBusy && _writeController.progress != null)
|
||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
|
@ -88,9 +88,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(icon: const Icon(Symbols.home)),
|
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: const Icon(Symbols.group)),
|
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: const Icon(Symbols.settings)),
|
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -315,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
}
|
}
|
||||||
|
|
||||||
return MaterialDesktopVideoControlsTheme(
|
return MaterialDesktopVideoControlsTheme(
|
||||||
|
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
|
||||||
normal: MaterialDesktopVideoControlsThemeData(
|
normal: MaterialDesktopVideoControlsThemeData(
|
||||||
buttonBarButtonSize: 24,
|
buttonBarButtonSize: 24,
|
||||||
buttonBarButtonColor: Colors.white,
|
buttonBarButtonColor: Colors.white,
|
||||||
@ -324,14 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
MaterialDesktopCustomButton(
|
MaterialDesktopCustomButton(
|
||||||
iconSize: 24,
|
iconSize: 24,
|
||||||
onPressed: _toggleOriginal,
|
onPressed: _toggleOriginal,
|
||||||
icon: Builder(builder: (context) {
|
icon: Icon(
|
||||||
return _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24);
|
_showOriginal ? Symbols.high_quality : Symbols.sd,
|
||||||
}),
|
size: 24,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
||||||
child: MaterialVideoControlsTheme(
|
child: MaterialVideoControlsTheme(
|
||||||
|
key: Key('material-video-controls-theme-$_showOriginal'),
|
||||||
normal: MaterialVideoControlsThemeData(
|
normal: MaterialVideoControlsThemeData(
|
||||||
buttonBarButtonSize: 24,
|
buttonBarButtonSize: 24,
|
||||||
buttonBarButtonColor: Colors.white,
|
buttonBarButtonColor: Colors.white,
|
||||||
|
@ -15,20 +15,22 @@ class AttachmentList extends StatefulWidget {
|
|||||||
final List<SnAttachment?> data;
|
final List<SnAttachment?> data;
|
||||||
final bool bordered;
|
final bool bordered;
|
||||||
final bool gridded;
|
final bool gridded;
|
||||||
final bool noGrow;
|
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final double? maxHeight;
|
final double? maxHeight;
|
||||||
final EdgeInsets? listPadding;
|
final double? minWidth;
|
||||||
|
final double? maxWidth;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const AttachmentList({
|
const AttachmentList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
this.bordered = false,
|
this.bordered = false,
|
||||||
this.gridded = false,
|
this.gridded = false,
|
||||||
this.noGrow = false,
|
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.maxHeight,
|
this.maxHeight,
|
||||||
this.listPadding,
|
this.minWidth,
|
||||||
|
this.maxWidth,
|
||||||
|
this.padding,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
||||||
@ -43,8 +45,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
(_) => const Uuid().v4(),
|
(_) => const Uuid().v4(),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const double kAttachmentMaxWidth = 640;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
@ -53,8 +53,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
||||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||||
final constraints = BoxConstraints(
|
final constraints = BoxConstraints(
|
||||||
minWidth: 80,
|
minWidth: widget.minWidth ?? 80,
|
||||||
maxHeight: widget.maxHeight ?? double.infinity,
|
maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||||
@ -67,11 +67,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
}
|
}
|
||||||
.toDouble();
|
.toDouble();
|
||||||
|
|
||||||
return Padding(
|
return Container(
|
||||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
child: Container(
|
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
width: double.infinity,
|
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: singleAspectRatio,
|
aspectRatio: singleAspectRatio,
|
||||||
@ -104,14 +102,12 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.gridded) {
|
if (widget.gridded) {
|
||||||
return Padding(
|
return Container(
|
||||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
margin: widget.padding ?? EdgeInsets.zero,
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
border: Border(
|
border: Border(
|
||||||
@ -134,7 +130,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
child: AttachmentItem(
|
child: AttachmentItem(
|
||||||
data: ele,
|
data: ele,
|
||||||
heroTag: heroTags[idx],
|
heroTag: heroTags[idx],
|
||||||
fit: widget.fit,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -154,22 +150,20 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AspectRatio(
|
return Container(
|
||||||
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(),
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior: _AttachmentListScrollBehavior(),
|
behavior: _AttachmentListScrollBehavior(),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
padding: widget.padding,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: widget.data.length,
|
itemCount: widget.data.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: constraints,
|
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@ -177,8 +171,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentZoomView(
|
AttachmentZoomView(
|
||||||
data:
|
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||||
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
|
||||||
initialIndex: idx,
|
initialIndex: idx,
|
||||||
heroTags: heroTags,
|
heroTags: heroTags,
|
||||||
),
|
),
|
||||||
@ -220,12 +213,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
padding: widget.listPadding,
|
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -231,7 +231,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
children: [
|
children: [
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: account!.avatar,
|
content: account?.avatar,
|
||||||
radius: 19,
|
radius: 19,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -246,7 +246,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
account.nick,
|
account?.nick ?? 'unknown'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
class PendingAttachmentAltDialog extends StatefulWidget {
|
||||||
|
final PostWriteMedia media;
|
||||||
|
const PendingAttachmentAltDialog({super.key, required this.media});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
|
||||||
|
final _contentController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_contentController.text = widget.media.attachment!.alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _performAction() async {
|
||||||
|
if (_isBusy) return;
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final result = await attach.updateOne(
|
||||||
|
widget.media.attachment!,
|
||||||
|
alt: _contentController.text,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
attach.putCache([result]);
|
||||||
|
Navigator.pop(context, result);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_contentController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('attachmentSetAlt').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _contentController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fieldAttachmentAlt'.tr(),
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performAction(),
|
||||||
|
child: Text('dialogConfirm'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
final Function(SnChatMessage)? onReply;
|
final Function(SnChatMessage)? onReply;
|
||||||
final Function(SnChatMessage)? onEdit;
|
final Function(SnChatMessage)? onEdit;
|
||||||
final Function(SnChatMessage)? onDelete;
|
final Function(SnChatMessage)? onDelete;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
const ChatMessage({
|
const ChatMessage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -35,6 +36,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
this.onReply,
|
this.onReply,
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
|
this.padding = const EdgeInsets.only(left: 12, right: 12),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -53,7 +55,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
iconOnRightSwipe: Symbols.edit,
|
iconOnRightSwipe: Symbols.edit,
|
||||||
swipeSensitivity: 20,
|
swipeSensitivity: 20,
|
||||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||||
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
|
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||||
child: ContextMenuArea(
|
child: ContextMenuArea(
|
||||||
contextMenu: ContextMenu(
|
contextMenu: ContextMenu(
|
||||||
entries: [
|
entries: [
|
||||||
@ -87,7 +89,9 @@ class ChatMessage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Padding(
|
||||||
|
padding: isCompact ? EdgeInsets.zero : padding,
|
||||||
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!isMerged && !isCompact)
|
if (!isMerged && !isCompact)
|
||||||
@ -98,29 +102,30 @@ class ChatMessage extends StatelessWidget {
|
|||||||
const Gap(40),
|
const Gap(40),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 480),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!isMerged)
|
if (!isMerged)
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
|
||||||
children: [
|
children: [
|
||||||
if (isCompact)
|
if (isCompact)
|
||||||
AccountImage(
|
AccountImage(
|
||||||
content: user?.avatar,
|
content: user?.avatar,
|
||||||
radius: 12,
|
radius: 12,
|
||||||
).padding(right: 6),
|
).padding(right: 8),
|
||||||
Text(
|
Text(
|
||||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
||||||
).bold(),
|
).bold(),
|
||||||
const Gap(6),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
dateFormatter.format(data.createdAt.toLocal()),
|
dateFormatter.format(data.createdAt.toLocal()),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isCompact) const Gap(4),
|
if (isCompact) const Gap(8),
|
||||||
if (data.preload?.quoteEvent != null)
|
if (data.preload?.quoteEvent != null)
|
||||||
StyledWidget(Container(
|
StyledWidget(Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -150,21 +155,23 @@ class ChatMessage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).opacity(isPending ? 0.5 : 1),
|
).opacity(isPending ? 0.5 : 1),
|
||||||
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
|
),
|
||||||
|
if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
|
||||||
LinkPreviewWidget(text: data.body['text']!),
|
LinkPreviewWidget(text: data.body['text']!),
|
||||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
gridded: true,
|
maxHeight: 560,
|
||||||
noGrow: true,
|
maxWidth: 480,
|
||||||
maxHeight: 520,
|
minWidth: 480,
|
||||||
listPadding: const EdgeInsets.only(top: 8),
|
padding: padding.copyWith(top: 8),
|
||||||
),
|
),
|
||||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
|
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -11,7 +11,6 @@ import 'package:surface/providers/user_directory.dart';
|
|||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
|
||||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||||
|
|
||||||
class ChatMessageInput extends StatefulWidget {
|
class ChatMessageInput extends StatefulWidget {
|
||||||
@ -33,6 +32,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
final TextEditingController _contentController = TextEditingController();
|
final TextEditingController _contentController = TextEditingController();
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_contentController.addListener(() {
|
||||||
|
if (_contentController.text.isNotEmpty) {
|
||||||
|
widget.controller.pingTypingStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void setReply(SnChatMessage? value) {
|
void setReply(SnChatMessage? value) {
|
||||||
setState(() => _replyingMessage = value);
|
setState(() => _replyingMessage = value);
|
||||||
}
|
}
|
||||||
@ -161,75 +170,82 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Padding(
|
|
||||||
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
|
||||||
child: _replyingMessage != null
|
child: _replyingMessage != null
|
||||||
? MaterialBanner(
|
? Container(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
leading: const Icon(Symbols.reply),
|
decoration: BoxDecoration(
|
||||||
backgroundColor: Colors.transparent,
|
border: Border(
|
||||||
content: SingleChildScrollView(
|
bottom: BorderSide(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
color: Theme.of(context).dividerColor,
|
||||||
child: Column(
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (_replyingMessage?.body['text'] != null)
|
const Icon(Symbols.reply, size: 20),
|
||||||
MarkdownTextContent(
|
const Gap(8),
|
||||||
content: _replyingMessage?.body['text'],
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
],
|
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
const Gap(16),
|
||||||
TextButton(
|
InkWell(
|
||||||
child: Text('cancel'.tr()),
|
child: Text('cancel'.tr()),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
setState(() => _replyingMessage = null);
|
setState(() => _replyingMessage = null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
).padding(vertical: 8),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.height(_replyingMessage != null ? 54 + 8 : 0, animate: true)
|
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Padding(
|
|
||||||
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
|
||||||
child: _editingMessage != null
|
child: _editingMessage != null
|
||||||
? MaterialBanner(
|
? Container(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
leading: const Icon(Symbols.edit),
|
decoration: BoxDecoration(
|
||||||
backgroundColor: Colors.transparent,
|
border: Border(
|
||||||
content: SingleChildScrollView(
|
bottom: BorderSide(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
color: Theme.of(context).dividerColor,
|
||||||
child: Column(
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (_editingMessage?.body['text'] != null)
|
const Icon(Symbols.edit, size: 20),
|
||||||
MarkdownTextContent(
|
const Gap(8),
|
||||||
content: _editingMessage?.body['text'],
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
],
|
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
const Gap(16),
|
||||||
TextButton(
|
InkWell(
|
||||||
child: Text('cancel'.tr()),
|
child: Text('cancel'.tr()),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
|
_contentController.clear();
|
||||||
setState(() => _editingMessage = null);
|
setState(() => _editingMessage = null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
).padding(vertical: 8),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.height(_editingMessage != null ? 54 + 8 : 0, animate: true)
|
.height(_editingMessage != null ? 38 : 0, animate: true)
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 56,
|
height: 56,
|
||||||
|
53
lib/widgets/chat/chat_typing_indicator.dart
Normal file
53
lib/widgets/chat/chat_typing_indicator.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/controllers/chat_message_controller.dart';
|
||||||
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
|
||||||
|
class ChatTypingIndicator extends StatelessWidget {
|
||||||
|
final ChatMessageController controller;
|
||||||
|
|
||||||
|
const ChatTypingIndicator({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
|
return StyledWidget(controller.typingMembers.isEmpty
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'messageTyping'.plural(controller.typingMembers.length, args: [
|
||||||
|
controller.typingMembers
|
||||||
|
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
||||||
|
? ele.nick!
|
||||||
|
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
|
||||||
|
.join(', '),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.height(controller.typingMembers.isNotEmpty ? 38 : 0, animate: true)
|
||||||
|
.animate(
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
Curves.fastLinearToSlowEaseIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -253,8 +253,9 @@ class PostItem extends StatelessWidget {
|
|||||||
bordered: true,
|
bordered: true,
|
||||||
gridded: true,
|
gridded: true,
|
||||||
maxHeight: showFullPost ? null : 480,
|
maxHeight: showFullPost ? null : 480,
|
||||||
|
minWidth: 640,
|
||||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
if (data.body['content'] != null)
|
if (data.body['content'] != null)
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
@ -336,10 +337,10 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
||||||
AttachmentList(
|
StyledWidget(AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
gridded: true,
|
gridded: true,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
)).padding(horizontal: 16, bottom: 8),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -930,9 +931,10 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
child: AttachmentList(
|
child: AttachmentList(
|
||||||
data: child.preload!.attachments!,
|
data: child.preload!.attachments!,
|
||||||
maxHeight: 360,
|
maxHeight: 360,
|
||||||
|
minWidth: 640,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
gridded: true,
|
gridded: true,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
).padding(
|
).padding(
|
||||||
top: 8,
|
top: 8,
|
||||||
|
@ -21,6 +21,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_input.dart';
|
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||||
|
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||||
import 'package:surface/widgets/context_menu.dart';
|
import 'package:surface/widgets/context_menu.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@ -157,6 +158,16 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
onUpdate!(idx, result);
|
onUpdate!(idx, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _setAlt(BuildContext context, int idx) async {
|
||||||
|
final result = await showDialog<SnAttachment?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
onUpdate!(idx, PostWriteMedia(result));
|
||||||
|
}
|
||||||
|
|
||||||
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
||||||
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||||
return ContextMenu(
|
return ContextMenu(
|
||||||
@ -169,6 +180,14 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
_compressVideo(context, idx);
|
_compressVideo(context, idx);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentSetAlt'.tr(),
|
||||||
|
icon: Symbols.description,
|
||||||
|
onSelected: () {
|
||||||
|
_setAlt(context, idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (media.attachment != null)
|
if (media.attachment != null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'attachmentBoost'.tr(),
|
label: 'attachmentBoost'.tr(),
|
||||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.2.1+41
|
version: 2.2.1+44
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@ -53,7 +53,6 @@ dependencies:
|
|||||||
markdown: ^7.2.2
|
markdown: ^7.2.2
|
||||||
flutter_markdown: ^0.7.4+1
|
flutter_markdown: ^0.7.4+1
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
cached_network_image: ^3.4.1
|
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.0
|
||||||
syntax_highlight: ^0.4.0
|
syntax_highlight: ^0.4.0
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
@ -116,6 +115,7 @@ dependencies:
|
|||||||
flutter_webrtc: ^0.12.5+hotfix.1
|
flutter_webrtc: ^0.12.5+hotfix.1
|
||||||
slide_countdown: ^2.0.2
|
slide_countdown: ^2.0.2
|
||||||
video_compress: ^3.1.3
|
video_compress: ^3.1.3
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user