✨ Chat basis
This commit is contained in:
parent
657f36c1f8
commit
5b45718ebd
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
|
import 'package:solian/models/realm.dart';
|
||||||
|
|
||||||
class Channel {
|
class Channel {
|
||||||
int id;
|
int id;
|
||||||
@ -13,6 +14,7 @@ class Channel {
|
|||||||
int type;
|
int type;
|
||||||
Account account;
|
Account account;
|
||||||
int accountId;
|
int accountId;
|
||||||
|
Realm? realm;
|
||||||
int? realmId;
|
int? realmId;
|
||||||
bool isEncrypted;
|
bool isEncrypted;
|
||||||
|
|
||||||
@ -30,6 +32,7 @@ class Channel {
|
|||||||
required this.account,
|
required this.account,
|
||||||
required this.accountId,
|
required this.accountId,
|
||||||
required this.isEncrypted,
|
required this.isEncrypted,
|
||||||
|
this.realm,
|
||||||
this.realmId,
|
this.realmId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -44,6 +47,7 @@ class Channel {
|
|||||||
type: json['type'],
|
type: json['type'],
|
||||||
account: Account.fromJson(json['account']),
|
account: Account.fromJson(json['account']),
|
||||||
accountId: json['account_id'],
|
accountId: json['account_id'],
|
||||||
|
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
|
||||||
realmId: json['realm_id'],
|
realmId: json['realm_id'],
|
||||||
isEncrypted: json['is_encrypted'],
|
isEncrypted: json['is_encrypted'],
|
||||||
);
|
);
|
||||||
@ -57,8 +61,9 @@ class Channel {
|
|||||||
'name': name,
|
'name': name,
|
||||||
'description': description,
|
'description': description,
|
||||||
'type': type,
|
'type': type,
|
||||||
'account': account,
|
'account': account.toJson(),
|
||||||
'account_id': accountId,
|
'account_id': accountId,
|
||||||
|
'realm': realm?.toJson(),
|
||||||
'realm_id': realmId,
|
'realm_id': realmId,
|
||||||
'is_encrypted': isEncrypted,
|
'is_encrypted': isEncrypted,
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@ class Message {
|
|||||||
DateTime createdAt;
|
DateTime createdAt;
|
||||||
DateTime updatedAt;
|
DateTime updatedAt;
|
||||||
DateTime? deletedAt;
|
DateTime? deletedAt;
|
||||||
String rawContent;
|
Map<String, dynamic> content;
|
||||||
Map<String, dynamic>? metadata;
|
Map<String, dynamic>? metadata;
|
||||||
String type;
|
String type;
|
||||||
List<String>? attachments;
|
List<String>? attachments;
|
||||||
@ -21,16 +21,12 @@ class Message {
|
|||||||
|
|
||||||
bool isSending = false;
|
bool isSending = false;
|
||||||
|
|
||||||
Map<String, dynamic> get decodedContent {
|
|
||||||
return jsonDecode(utf8.fuse(base64).decode(rawContent));
|
|
||||||
}
|
|
||||||
|
|
||||||
Message({
|
Message({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
required this.rawContent,
|
required this.content,
|
||||||
required this.metadata,
|
required this.metadata,
|
||||||
required this.type,
|
required this.type,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
@ -47,14 +43,16 @@ class Message {
|
|||||||
createdAt: DateTime.parse(json['created_at']),
|
createdAt: DateTime.parse(json['created_at']),
|
||||||
updatedAt: DateTime.parse(json['updated_at']),
|
updatedAt: DateTime.parse(json['updated_at']),
|
||||||
deletedAt: json['deleted_at'],
|
deletedAt: json['deleted_at'],
|
||||||
rawContent: json['content'],
|
content: json['content'],
|
||||||
metadata: json['metadata'],
|
metadata: json['metadata'],
|
||||||
type: json['type'],
|
type: json['type'],
|
||||||
attachments: json['attachments'],
|
attachments: json['attachments'],
|
||||||
channel: Channel.fromJson(json['channel']),
|
channel: Channel.fromJson(json['channel']),
|
||||||
sender: Sender.fromJson(json['sender']),
|
sender: Sender.fromJson(json['sender']),
|
||||||
replyId: json['reply_id'],
|
replyId: json['reply_id'],
|
||||||
replyTo: json['reply_to'] != null ? Message.fromJson(json['reply_to']) : null,
|
replyTo: json['reply_to'] != null
|
||||||
|
? Message.fromJson(json['reply_to'])
|
||||||
|
: null,
|
||||||
channelId: json['channel_id'],
|
channelId: json['channel_id'],
|
||||||
senderId: json['sender_id'],
|
senderId: json['sender_id'],
|
||||||
);
|
);
|
||||||
@ -64,7 +62,7 @@ class Message {
|
|||||||
'created_at': createdAt.toIso8601String(),
|
'created_at': createdAt.toIso8601String(),
|
||||||
'updated_at': updatedAt.toIso8601String(),
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
'deleted_at': deletedAt,
|
'deleted_at': deletedAt,
|
||||||
'content': rawContent,
|
'content': content,
|
||||||
'metadata': metadata,
|
'metadata': metadata,
|
||||||
'type': type,
|
'type': type,
|
||||||
'attachments': attachments,
|
'attachments': attachments,
|
||||||
|
@ -10,7 +10,7 @@ class Realm {
|
|||||||
String description;
|
String description;
|
||||||
bool isPublic;
|
bool isPublic;
|
||||||
bool isCommunity;
|
bool isCommunity;
|
||||||
int accountId;
|
int? accountId;
|
||||||
|
|
||||||
Realm({
|
Realm({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -22,14 +22,16 @@ class Realm {
|
|||||||
required this.description,
|
required this.description,
|
||||||
required this.isPublic,
|
required this.isPublic,
|
||||||
required this.isCommunity,
|
required this.isCommunity,
|
||||||
required this.accountId,
|
this.accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Realm.fromJson(Map<String, dynamic> json) => Realm(
|
factory Realm.fromJson(Map<String, dynamic> json) => Realm(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
createdAt: DateTime.parse(json['created_at']),
|
createdAt: DateTime.parse(json['created_at']),
|
||||||
updatedAt: DateTime.parse(json['updated_at']),
|
updatedAt: DateTime.parse(json['updated_at']),
|
||||||
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
|
deletedAt: json['deleted_at'] != null
|
||||||
|
? DateTime.parse(json['deleted_at'])
|
||||||
|
: null,
|
||||||
alias: json['alias'],
|
alias: json['alias'],
|
||||||
name: json['name'],
|
name: json['name'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
@ -77,7 +79,9 @@ class RealmMember {
|
|||||||
id: json['id'],
|
id: json['id'],
|
||||||
createdAt: DateTime.parse(json['created_at']),
|
createdAt: DateTime.parse(json['created_at']),
|
||||||
updatedAt: DateTime.parse(json['updated_at']),
|
updatedAt: DateTime.parse(json['updated_at']),
|
||||||
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
|
deletedAt: json['deleted_at'] != null
|
||||||
|
? DateTime.parse(json['deleted_at'])
|
||||||
|
: null,
|
||||||
realmId: json['realm_id'],
|
realmId: json['realm_id'],
|
||||||
accountId: json['account_id'],
|
accountId: json['account_id'],
|
||||||
account: Account.fromJson(json['account']),
|
account: Account.fromJson(json['account']),
|
||||||
|
@ -3,6 +3,21 @@ import 'package:solian/providers/auth.dart';
|
|||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
|
||||||
class ChannelProvider extends GetxController {
|
class ChannelProvider extends GetxController {
|
||||||
|
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
|
final client = GetConnect();
|
||||||
|
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||||
|
|
||||||
|
final resp = await client.get('/api/channels/$realm/$alias');
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw Exception(resp.bodyString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> listAvailableChannel({String realm = 'global'}) async {
|
Future<Response> listAvailableChannel({String realm = 'global'}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
@ -2,6 +2,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:solian/screens/account.dart';
|
import 'package:solian/screens/account.dart';
|
||||||
import 'package:solian/screens/account/friend.dart';
|
import 'package:solian/screens/account/friend.dart';
|
||||||
import 'package:solian/screens/account/personalize.dart';
|
import 'package:solian/screens/account/personalize.dart';
|
||||||
|
import 'package:solian/screens/channel/channel_chat.dart';
|
||||||
import 'package:solian/screens/channel/channel_organize.dart';
|
import 'package:solian/screens/channel/channel_organize.dart';
|
||||||
import 'package:solian/screens/contact.dart';
|
import 'package:solian/screens/contact.dart';
|
||||||
import 'package:solian/screens/posts/post_detail.dart';
|
import 'package:solian/screens/posts/post_detail.dart';
|
||||||
@ -87,6 +88,16 @@ abstract class AppRouter {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/chat/:alias',
|
||||||
|
name: 'channelChat',
|
||||||
|
builder: (context, state) {
|
||||||
|
return ChannelChatScreen(
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
|
realm: state.uri.queryParameters['realm'] ?? 'global',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
171
lib/screens/channel/channel_chat.dart
Normal file
171
lib/screens/channel/channel_chat.dart
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/channel.dart';
|
||||||
|
import 'package:solian/models/message.dart';
|
||||||
|
import 'package:solian/models/pagination.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/content/channel.dart';
|
||||||
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/chat/chat_message.dart';
|
||||||
|
|
||||||
|
class ChannelChatScreen extends StatefulWidget {
|
||||||
|
final String alias;
|
||||||
|
final String realm;
|
||||||
|
|
||||||
|
const ChannelChatScreen({
|
||||||
|
super.key,
|
||||||
|
required this.alias,
|
||||||
|
this.realm = 'global',
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Channel? _channel;
|
||||||
|
|
||||||
|
final PagingController<int, Message> _pagingController =
|
||||||
|
PagingController(firstPageKey: 0);
|
||||||
|
|
||||||
|
getChannel() async {
|
||||||
|
final ChannelProvider provider = Get.find();
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final resp = await provider.getChannel(widget.alias, realm: widget.realm);
|
||||||
|
setState(() => _channel = Channel.fromJson(resp.body));
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> getMessages(int pageKey) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (!await auth.isAuthorized) return;
|
||||||
|
|
||||||
|
final client = GetConnect();
|
||||||
|
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||||
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
final resp = await client.get(
|
||||||
|
'/api/channels/${widget.realm}/${widget.alias}/messages?take=10&offset=$pageKey');
|
||||||
|
|
||||||
|
if (resp.statusCode == 200) {
|
||||||
|
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||||
|
final parsed = result.data?.map((e) => Message.fromJson(e)).toList();
|
||||||
|
|
||||||
|
if (parsed != null && parsed.length >= 10) {
|
||||||
|
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
||||||
|
} else if (parsed != null) {
|
||||||
|
_pagingController.appendLastPage(parsed);
|
||||||
|
}
|
||||||
|
} else if (resp.statusCode == 403) {
|
||||||
|
_pagingController.appendLastPage([]);
|
||||||
|
} else {
|
||||||
|
_pagingController.error = resp.bodyString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkMessageMergeable(Message? a, Message? b) {
|
||||||
|
if (a?.replyTo != null) return false;
|
||||||
|
if (a == null || b == null) return false;
|
||||||
|
if (a.senderId != b.senderId) return false;
|
||||||
|
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget chatHistoryBuilder(context, item, index) {
|
||||||
|
bool isMerged = false, hasMerged = false;
|
||||||
|
if (index > 0) {
|
||||||
|
hasMerged = checkMessageMergeable(
|
||||||
|
_pagingController.itemList?[index - 1],
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
|
||||||
|
isMerged = checkMessageMergeable(
|
||||||
|
item,
|
||||||
|
_pagingController.itemList?[index + 1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return InkWell(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: !isMerged ? 8 : 0,
|
||||||
|
bottom: !hasMerged ? 8 : 0,
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
),
|
||||||
|
child: ChatMessage(
|
||||||
|
item: item,
|
||||||
|
isCompact: isMerged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onLongPress: () {},
|
||||||
|
).animate(key: Key('m${item.id}'), autoPlay: true).slideY(
|
||||||
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
duration: 350.ms,
|
||||||
|
begin: 0.25,
|
||||||
|
end: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
getChannel().then((_) {
|
||||||
|
_pagingController.addPageRequestListener(getMessages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_isBusy) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_channel?.name ?? 'loading'.tr),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: PagedListView<int, Message>(
|
||||||
|
reverse: true,
|
||||||
|
pagingController: _pagingController,
|
||||||
|
builderDelegate: PagedChildBuilderDelegate<Message>(
|
||||||
|
animateTransitions: true,
|
||||||
|
transitionDuration: 350.ms,
|
||||||
|
itemBuilder: chatHistoryBuilder,
|
||||||
|
noItemsFoundIndicatorBuilder: (_) => Container(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -78,8 +78,9 @@ class _ContactScreenState extends State<ContactScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!SolianTheme.isLargeScreen(context))
|
SizedBox(
|
||||||
const SizedBox(width: 16),
|
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -111,7 +112,16 @@ class _ContactScreenState extends State<ContactScreen> {
|
|||||||
const EdgeInsets.symmetric(horizontal: 24),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
title: Text(element.name),
|
title: Text(element.name),
|
||||||
subtitle: Text(element.description),
|
subtitle: Text(element.description),
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed(
|
||||||
|
'channelChat',
|
||||||
|
pathParameters: {'alias': element.alias},
|
||||||
|
queryParameters: {
|
||||||
|
if (element.realmId != null)
|
||||||
|
'realm': element.realm!.alias,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -87,8 +87,9 @@ class _SocialScreenState extends State<SocialScreen> {
|
|||||||
forceElevated: innerBoxIsScrolled,
|
forceElevated: innerBoxIsScrolled,
|
||||||
actions: [
|
actions: [
|
||||||
const NotificationButton(),
|
const NotificationButton(),
|
||||||
if (!SolianTheme.isLargeScreen(context))
|
SizedBox(
|
||||||
const SizedBox(width: 16),
|
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -14,6 +14,7 @@ class SolianMessages extends Translations {
|
|||||||
'apply': 'Apply',
|
'apply': 'Apply',
|
||||||
'cancel': 'Cancel',
|
'cancel': 'Cancel',
|
||||||
'confirm': 'Confirm',
|
'confirm': 'Confirm',
|
||||||
|
'loading': 'Loading...',
|
||||||
'edit': 'Edit',
|
'edit': 'Edit',
|
||||||
'delete': 'Delete',
|
'delete': 'Delete',
|
||||||
'search': 'Search',
|
'search': 'Search',
|
||||||
@ -100,6 +101,8 @@ class SolianMessages extends Translations {
|
|||||||
'channelType': 'Channel type',
|
'channelType': 'Channel type',
|
||||||
'channelTypeCommon': 'Regular',
|
'channelTypeCommon': 'Regular',
|
||||||
'channelTypeDirect': 'DM',
|
'channelTypeDirect': 'DM',
|
||||||
|
'messageDecoding': 'Decoding...',
|
||||||
|
'messageDecodeFailed': 'Unable to decode: @message',
|
||||||
},
|
},
|
||||||
'zh_CN': {
|
'zh_CN': {
|
||||||
'hide': '隐藏',
|
'hide': '隐藏',
|
||||||
@ -108,6 +111,7 @@ class SolianMessages extends Translations {
|
|||||||
'reset': '重置',
|
'reset': '重置',
|
||||||
'cancel': '取消',
|
'cancel': '取消',
|
||||||
'confirm': '确认',
|
'confirm': '确认',
|
||||||
|
'loading': '载入中…',
|
||||||
'edit': '编辑',
|
'edit': '编辑',
|
||||||
'delete': '删除',
|
'delete': '删除',
|
||||||
'page': '页面',
|
'page': '页面',
|
||||||
@ -191,6 +195,8 @@ class SolianMessages extends Translations {
|
|||||||
'channelType': '频道类型',
|
'channelType': '频道类型',
|
||||||
'channelTypeCommon': '普通频道',
|
'channelTypeCommon': '普通频道',
|
||||||
'channelTypeDirect': '私信聊天',
|
'channelTypeDirect': '私信聊天',
|
||||||
|
'messageDecoding': '解码信息中…',
|
||||||
|
'messageDecodeFailed': '解码信息失败:@message',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
104
lib/widgets/chat/chat_message.dart
Normal file
104
lib/widgets/chat/chat_message.dart
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/message.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:timeago/timeago.dart' show format;
|
||||||
|
|
||||||
|
class ChatMessage extends StatelessWidget {
|
||||||
|
final Message item;
|
||||||
|
final bool isCompact;
|
||||||
|
|
||||||
|
const ChatMessage({super.key, required this.item, required this.isCompact});
|
||||||
|
|
||||||
|
Future<String?> decodeContent(Map<String, dynamic> content) async {
|
||||||
|
String? text;
|
||||||
|
if (item.type == 'm.text') {
|
||||||
|
switch (content['algorithm']) {
|
||||||
|
case 'plain':
|
||||||
|
text = content['value'];
|
||||||
|
default:
|
||||||
|
throw Exception('Unsupported algorithm');
|
||||||
|
// TODO Impl AES algorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasAttachment = item.attachments?.isNotEmpty ?? false;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AccountAvatar(content: item.sender.account.avatar),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.sender.account.nick,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(format(item.createdAt, locale: 'en_short'))
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 12),
|
||||||
|
FutureBuilder(
|
||||||
|
future: decodeContent(item.content),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: 0.8,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.more_horiz),
|
||||||
|
Text('messageDecoding'.tr)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate(onPlay: (c) => c.repeat())
|
||||||
|
.fade(begin: 0, end: 1);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: 0.9,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.close),
|
||||||
|
Text(
|
||||||
|
'messageDecodeFailed'.trParams(
|
||||||
|
{'message': snapshot.error.toString()}),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Markdown(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
data: snapshot.data ?? '',
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
).paddingOnly(
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
top: 2,
|
||||||
|
bottom: hasAttachment ? 4 : 0,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,18 +15,21 @@ class _AppNavigationBottomBarState extends State<AppNavigationBottomBar> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BottomNavigationBar(
|
return BottomNavigationBar(
|
||||||
items: AppNavigation.destinations.map(
|
items: AppNavigation.destinations
|
||||||
(e) => BottomNavigationBarItem(
|
.map(
|
||||||
icon: e.icon,
|
(e) => BottomNavigationBarItem(
|
||||||
label: e.label,
|
icon: e.icon,
|
||||||
),
|
label: e.label,
|
||||||
).toList(),
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
|
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
|
||||||
currentIndex: _selectedIndex,
|
currentIndex: _selectedIndex,
|
||||||
showUnselectedLabels: false,
|
showUnselectedLabels: false,
|
||||||
onTap: (idx) {
|
onTap: (idx) {
|
||||||
setState(() => _selectedIndex = idx);
|
setState(() => _selectedIndex = idx);
|
||||||
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
AppRouter.instance
|
||||||
|
.pushReplacementNamed(AppNavigation.destinations[idx].page);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,8 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
|
|||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onDestinationSelected: (idx) {
|
onDestinationSelected: (idx) {
|
||||||
setState(() => _selectedIndex = idx);
|
setState(() => _selectedIndex = idx);
|
||||||
AppRouter.instance.pushNamed(AppNavigation.destinations[idx].page);
|
AppRouter.instance
|
||||||
|
.pushReplacementNamed(AppNavigation.destinations[idx].page);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- file_selector_macos (0.0.1):
|
- file_selector_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- flutter_local_notifications (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- flutter_secure_storage_macos (6.1.1):
|
- flutter_secure_storage_macos (6.1.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
@ -12,6 +14,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||||
|
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
@ -20,6 +23,8 @@ DEPENDENCIES:
|
|||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
file_selector_macos:
|
file_selector_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||||
|
flutter_local_notifications:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||||
flutter_secure_storage_macos:
|
flutter_secure_storage_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
@ -31,6 +36,7 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||||
|
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||||
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
|
@ -120,7 +120,6 @@
|
|||||||
1B3B8DF79807852D828EBE0C /* Pods-RunnerTests.release.xcconfig */,
|
1B3B8DF79807852D828EBE0C /* Pods-RunnerTests.release.xcconfig */,
|
||||||
BA5247A2B03173FDFDFCFF93 /* Pods-RunnerTests.profile.xcconfig */,
|
BA5247A2B03173FDFDFCFF93 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -724,13 +723,17 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
|
@ -22,6 +22,11 @@
|
|||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict/>
|
||||||
|
</dict>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||||
<key>NSMainNibFile</key>
|
<key>NSMainNibFile</key>
|
||||||
|
@ -4,5 +4,13 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.device.bluetooth</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
Loading…
Reference in New Issue
Block a user