💄 Better chat list
This commit is contained in:
parent
147879e4d8
commit
f50461a7f7
@ -464,5 +464,7 @@
|
|||||||
"blockUser": "Block user",
|
"blockUser": "Block user",
|
||||||
"unblockUser": "Unblock user",
|
"unblockUser": "Unblock user",
|
||||||
"learnMoreAboutPerson": "Learn more about that person",
|
"learnMoreAboutPerson": "Learn more about that person",
|
||||||
"global": "Global"
|
"global": "Global",
|
||||||
|
"all": "All",
|
||||||
|
"unablePreview": "Unable to preview"
|
||||||
}
|
}
|
||||||
|
@ -460,5 +460,7 @@
|
|||||||
"blockUser": "屏蔽用户",
|
"blockUser": "屏蔽用户",
|
||||||
"unblockUser": "解除屏蔽用户",
|
"unblockUser": "解除屏蔽用户",
|
||||||
"learnMoreAboutPerson": "了解关于 TA 的更多",
|
"learnMoreAboutPerson": "了解关于 TA 的更多",
|
||||||
"global": "全局"
|
"global": "全局",
|
||||||
|
"all": "全部",
|
||||||
|
"unablePreview": "无法预览"
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,7 @@ class ChannelProvider extends GetxController {
|
|||||||
final resp = await listAvailableChannel();
|
final resp = await listAvailableChannel();
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
availableChannels.value =
|
availableChannels.value = await listAvailableChannel();
|
||||||
resp.body.map((x) => Channel.fromJson(x)).toList().cast<Channel>();
|
|
||||||
availableChannels.refresh();
|
availableChannels.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,18 +88,22 @@ class ChannelProvider extends GetxController {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> listAvailableChannel({String scope = 'global'}) async {
|
Future<List<Channel>> listAvailableChannel({
|
||||||
|
String scope = 'global',
|
||||||
|
bool isDirect = false,
|
||||||
|
}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||||
|
|
||||||
final client = await auth.configureClient('messaging');
|
final client = await auth.configureClient('messaging');
|
||||||
|
|
||||||
final resp = await client.get('/channels/$scope/me/available');
|
final resp =
|
||||||
|
await client.get('/channels/$scope/me/available?direct=$isDirect');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return List.from(resp.body.map((x) => Channel.fromJson(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> createChannel(String scope, dynamic payload) async {
|
Future<Response> createChannel(String scope, dynamic payload) async {
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
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:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
import 'package:solian/providers/content/channel.dart';
|
||||||
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/account/notification.dart';
|
import 'package:solian/screens/account/notification.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
import 'package:solian/widgets/channel/channel_list.dart';
|
||||||
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
const ChatScreen({super.key});
|
const ChatScreen({super.key});
|
||||||
@ -23,24 +28,84 @@ class ChatScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
class _ChatScreenState extends State<ChatScreen> {
|
||||||
late final ChannelProvider _channels;
|
List<Channel> _normalChannels = List.empty();
|
||||||
|
List<Channel> _directChannels = List.empty();
|
||||||
|
final Map<String, List<Channel>> _realmChannels = {};
|
||||||
|
|
||||||
|
late final ChannelProvider _channels = Get.find();
|
||||||
|
|
||||||
|
List<Channel> _sortChannels(List<Channel> channels) {
|
||||||
|
channels.sort(
|
||||||
|
(a, b) =>
|
||||||
|
_lastMessages?[b.id]?.createdAt.compareTo(
|
||||||
|
_lastMessages?[a.id]?.createdAt ??
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
) ??
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadNormalChannels() async {
|
||||||
|
final resp = await _channels.listAvailableChannel(isDirect: false);
|
||||||
|
setState(() {
|
||||||
|
_normalChannels = _sortChannels(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadDirectChannels() async {
|
||||||
|
final resp = await _channels.listAvailableChannel(isDirect: true);
|
||||||
|
setState(() {
|
||||||
|
_directChannels = _sortChannels(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRealmChannels(String realm) async {
|
||||||
|
final resp = await _channels.listAvailableChannel(scope: realm);
|
||||||
|
setState(() {
|
||||||
|
_realmChannels[realm] = _sortChannels(List.from(resp));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAllChannels() async {
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
|
Future.wait([
|
||||||
|
_loadNormalChannels(),
|
||||||
|
_loadDirectChannels(),
|
||||||
|
...realms.availableRealms.map((x) => _loadRealmChannels(x.alias)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||||
|
|
||||||
|
Future<void> _loadLastMessages() async {
|
||||||
|
final ctrl = ChatEventController();
|
||||||
|
await ctrl.initialize();
|
||||||
|
final messages = await ctrl.src.getLastInAllChannels();
|
||||||
|
setState(() {
|
||||||
|
_lastMessages = messages
|
||||||
|
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||||
|
.cast<int, LocalMessageEventTableData>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
try {
|
_loadLastMessages().then((_) {
|
||||||
_channels = Get.find();
|
_loadAllChannels();
|
||||||
_channels.refreshAvailableChannel();
|
});
|
||||||
} catch (e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
|
|
||||||
return Material(
|
return Obx(
|
||||||
|
() => DefaultTabController(
|
||||||
|
length: 2 + realms.availableRealms.length,
|
||||||
|
child: Material(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@ -67,7 +132,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text('channelOrganizeCommon'.tr),
|
title: Text('channelOrganizeCommon'.tr),
|
||||||
leading: const Icon(Icons.tag),
|
leading: const Icon(Icons.tag),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.pushNamed('channelOrganizing').then(
|
AppRouter.instance.pushNamed('channelOrganizing').then(
|
||||||
@ -86,7 +152,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
FontAwesomeIcons.userGroup,
|
FontAwesomeIcons.userGroup,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final ChannelProvider channels = Get.find();
|
final ChannelProvider channels = Get.find();
|
||||||
@ -107,11 +174,70 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
|
dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
|
tabAlignment: TabAlignment.startOffset,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.forum,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('all'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
child: Icon(
|
||||||
|
Icons.chat_bubble,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('channelTypeDirect'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...realms.availableRealms.map((x) => Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AccountAvatar(
|
||||||
|
content: x.avatar,
|
||||||
|
radius: 14,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(x.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Obx(() {
|
body: Obx(() {
|
||||||
if (auth.isAuthorized.isFalse) {
|
if (auth.isAuthorized.isFalse) {
|
||||||
return SigninRequiredOverlay(
|
return SigninRequiredOverlay(
|
||||||
onDone: () => _channels.refreshAvailableChannel(),
|
onDone: () => _loadAllChannels(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,28 +247,47 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const ChatCallCurrentIndicator(),
|
const ChatCallCurrentIndicator(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CenteredContainer(
|
child: TabBarView(
|
||||||
child: RefreshIndicator(
|
children: [
|
||||||
onRefresh: _channels.refreshAvailableChannel,
|
RefreshIndicator(
|
||||||
child: Obx(
|
onRefresh: _loadNormalChannels,
|
||||||
() => ChannelListWidget(
|
child: ChannelListWidget(
|
||||||
noCategory: true,
|
channels: _sortChannels([
|
||||||
channels: List.from([
|
..._normalChannels,
|
||||||
..._channels.groupChannels
|
..._directChannels,
|
||||||
.where((x) => x.realmId == null),
|
..._realmChannels.values.expand((x) => x),
|
||||||
..._channels.directChannels
|
|
||||||
]),
|
]),
|
||||||
selfId: selfId,
|
selfId: selfId,
|
||||||
useReplace: false,
|
useReplace: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: _loadDirectChannels,
|
||||||
|
child: ChannelListWidget(
|
||||||
|
channels: _directChannels,
|
||||||
|
selfId: selfId,
|
||||||
|
useReplace: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
...realms.availableRealms.map(
|
||||||
|
(x) => RefreshIndicator(
|
||||||
|
onRefresh: () => _loadRealmChannels(x.alias),
|
||||||
|
child: ChannelListWidget(
|
||||||
|
channels: _realmChannels[x.alias] ?? [],
|
||||||
|
selfId: selfId,
|
||||||
|
useReplace: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,6 +101,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
|
dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: 'postListNews'.tr),
|
Tab(text: 'postListNews'.tr),
|
||||||
Tab(text: 'postListFriends'.tr),
|
Tab(text: 'postListFriends'.tr),
|
||||||
|
@ -68,12 +68,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
|||||||
_channels.addAll(
|
_channels.addAll(
|
||||||
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
||||||
);
|
);
|
||||||
_channels.addAll(
|
_channels.addAll(availableResp);
|
||||||
availableResp.body
|
|
||||||
.map((e) => Channel.fromJson(e))
|
|
||||||
.toList()
|
|
||||||
.cast<Channel>(),
|
|
||||||
);
|
|
||||||
_channels.retainWhere((x) => channelIdx.add(x.id));
|
_channels.retainWhere((x) => channelIdx.add(x.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -260,7 +255,6 @@ class RealmChannelListWidget extends StatelessWidget {
|
|||||||
child: ChannelListWidget(
|
child: ChannelListWidget(
|
||||||
channels: channels,
|
channels: channels,
|
||||||
selfId: auth.userProfile.value!['id'],
|
selfId: auth.userProfile.value!['id'],
|
||||||
noCategory: true,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -4,19 +4,18 @@ 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:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/database/database.dart';
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
|
||||||
class ChannelListWidget extends StatefulWidget {
|
class ChannelListWidget extends StatefulWidget {
|
||||||
final List<Channel> channels;
|
final List<Channel> channels;
|
||||||
final int selfId;
|
final int selfId;
|
||||||
final bool isDense;
|
|
||||||
final bool isCollapsed;
|
|
||||||
final bool noCategory;
|
|
||||||
final bool useReplace;
|
final bool useReplace;
|
||||||
final Function(Channel)? onSelected;
|
final Function(Channel)? onSelected;
|
||||||
|
|
||||||
@ -24,9 +23,6 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.channels,
|
required this.channels,
|
||||||
required this.selfId,
|
required this.selfId,
|
||||||
this.isDense = false,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
this.noCategory = false,
|
|
||||||
this.useReplace = false,
|
this.useReplace = false,
|
||||||
this.onSelected,
|
this.onSelected,
|
||||||
});
|
});
|
||||||
@ -36,9 +32,6 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||||
final List<Channel> _globalChannels = List.empty(growable: true);
|
|
||||||
final Map<String, List<Channel>> _inRealms = {};
|
|
||||||
|
|
||||||
Map<int, LocalMessageEventTableData>? _lastMessages;
|
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||||
|
|
||||||
final ChatEventController _eventController = ChatEventController();
|
final ChatEventController _eventController = ChatEventController();
|
||||||
@ -52,37 +45,9 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _mapChannels() {
|
|
||||||
_inRealms.clear();
|
|
||||||
_globalChannels.clear();
|
|
||||||
|
|
||||||
if (widget.noCategory) {
|
|
||||||
_globalChannels.addAll(widget.channels);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final channel in widget.channels) {
|
|
||||||
if (channel.realmId != null) {
|
|
||||||
if (_inRealms[channel.realm!.alias] == null) {
|
|
||||||
_inRealms[channel.realm!.alias] = List.empty(growable: true);
|
|
||||||
}
|
|
||||||
_inRealms[channel.realm!.alias]!.add(channel);
|
|
||||||
} else {
|
|
||||||
_globalChannels.add(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant ChannelListWidget oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
setState(() => _mapChannels());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_mapChannels();
|
|
||||||
_eventController.initialize().then((_) {
|
_eventController.initialize().then((_) {
|
||||||
_loadLastMessages();
|
_loadLastMessages();
|
||||||
});
|
});
|
||||||
@ -112,7 +77,45 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChannelDescription(Channel item, ChannelMember? otherside) {
|
Widget _buildTitle(Channel item, ChannelMember? otherside) {
|
||||||
|
if (otherside != null) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(otherside.account.nick)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
|
Text(
|
||||||
|
DateFormat('MM/dd').format(
|
||||||
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(item.name)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
|
Text(
|
||||||
|
DateFormat('MM/dd').format(
|
||||||
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubtitle(Channel item, ChannelMember? otherside) {
|
||||||
if (PlatformInfo.isWeb) {
|
if (PlatformInfo.isWeb) {
|
||||||
return otherside != null
|
return otherside != null
|
||||||
? Text(
|
? Text(
|
||||||
@ -137,7 +140,8 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
},
|
},
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: (_lastMessages == null || _lastMessages![item.id] == null)
|
child: (_lastMessages == null || _lastMessages![item.id] == null)
|
||||||
? Builder(builder: (context) {
|
? Builder(
|
||||||
|
builder: (context) {
|
||||||
return otherside != null
|
return otherside != null
|
||||||
? Text(
|
? Text(
|
||||||
'channelDirectDescription'.trParams(
|
'channelDirectDescription'.trParams(
|
||||||
@ -151,14 +155,34 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
);
|
);
|
||||||
})
|
},
|
||||||
|
)
|
||||||
: Builder(
|
: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final data = _lastMessages![item.id]!.data!;
|
final data = _lastMessages![item.id]!.data!;
|
||||||
return Text(
|
return Row(
|
||||||
'${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}',
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (item.type == 0)
|
||||||
|
Badge(
|
||||||
|
label: Text(data.sender.account.nick),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
textColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
if (item.type == 0) const Gap(6),
|
||||||
|
if (data.body['text'] != null)
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
data.body['text'],
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Badge(label: Text('unablePreview'.tr)),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -175,9 +199,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEntry(Channel item) {
|
Widget _buildEntry(Channel item) {
|
||||||
final padding = widget.isDense
|
const padding = EdgeInsets.symmetric(horizontal: 20);
|
||||||
? const EdgeInsets.symmetric(horizontal: 20)
|
|
||||||
: const EdgeInsets.symmetric(horizontal: 16);
|
|
||||||
|
|
||||||
final otherside =
|
final otherside =
|
||||||
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
|
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
|
||||||
@ -185,60 +207,53 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
if (item.type == 1 && otherside != null) {
|
if (item.type == 1 && otherside != null) {
|
||||||
final avatar = AccountAvatar(
|
final avatar = AccountAvatar(
|
||||||
content: otherside.account.avatar,
|
content: otherside.account.avatar,
|
||||||
radius: widget.isDense ? 12 : 20,
|
radius: 20,
|
||||||
bgColor: Theme.of(context).colorScheme.primary,
|
bgColor: Theme.of(context).colorScheme.primary,
|
||||||
feColor: Theme.of(context).colorScheme.onPrimary,
|
feColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: otherside.account.nick,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: avatar,
|
leading: avatar,
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(otherside.account.nick),
|
title: _buildTitle(item, otherside),
|
||||||
subtitle:
|
subtitle: _buildSubtitle(item, otherside),
|
||||||
!widget.isDense ? _buildChannelDescription(item, otherside) : null,
|
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final avatar = CircleAvatar(
|
final avatar = CircleAvatar(
|
||||||
backgroundColor: item.realmId == null
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
? Theme.of(context).colorScheme.primary
|
radius: 20,
|
||||||
: Colors.transparent,
|
|
||||||
radius: widget.isDense ? 12 : 20,
|
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.hashtag,
|
FontAwesomeIcons.hashtag,
|
||||||
color: item.realmId == null
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
size: 16,
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: item.name,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
minTileHeight: widget.isDense ? 48 : null,
|
minTileHeight: null,
|
||||||
leading: avatar,
|
leading: item.realmId == null
|
||||||
|
? avatar
|
||||||
|
: badges.Badge(
|
||||||
|
position: badges.BadgePosition.bottomEnd(bottom: -4, end: -6),
|
||||||
|
badgeStyle: badges.BadgeStyle(
|
||||||
|
badgeColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
elevation: 8,
|
||||||
|
),
|
||||||
|
badgeContent: AccountAvatar(
|
||||||
|
content: item.realm?.avatar,
|
||||||
|
radius: 10,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: avatar,
|
||||||
|
),
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(item.name),
|
title: _buildTitle(item, null),
|
||||||
subtitle: !widget.isDense ? _buildChannelDescription(item, null) : null,
|
subtitle: _buildSubtitle(item, null),
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -246,13 +261,12 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.noCategory) {
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: _globalChannels.length,
|
itemCount: widget.channels.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final element = _globalChannels[index];
|
final element = widget.channels[index];
|
||||||
return _buildEntry(element);
|
return _buildEntry(element);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -260,36 +274,4 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: _globalChannels.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = _globalChannels[index];
|
|
||||||
return _buildEntry(element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SliverList.list(
|
|
||||||
children: _inRealms.entries.map((element) {
|
|
||||||
return ExpansionTile(
|
|
||||||
tilePadding: const EdgeInsets.only(left: 20, right: 24),
|
|
||||||
minTileHeight: 48,
|
|
||||||
title: Text(element.value.first.realm!.name),
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Colors.teal,
|
|
||||||
radius: widget.isDense ? 12 : 24,
|
|
||||||
child: Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: element.value.map((x) => _buildEntry(x)).toList(),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,230 +0,0 @@
|
|||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/realm.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/content/realm.dart';
|
|
||||||
import 'package:solian/providers/navigation.dart';
|
|
||||||
import 'package:solian/services.dart';
|
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
|
||||||
|
|
||||||
class AppNavigationRegion extends StatefulWidget {
|
|
||||||
final bool isCollapsed;
|
|
||||||
final Function onSelected;
|
|
||||||
|
|
||||||
const AppNavigationRegion({
|
|
||||||
super.key,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AppNavigationRegion> createState() => _AppNavigationRegionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppNavigationRegionState extends State<AppNavigationRegion> {
|
|
||||||
bool _isTryingExit = false;
|
|
||||||
|
|
||||||
void _focusRealm(Realm item) {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = item,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unFocusRealm() {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRealmFocusAvatar() {
|
|
||||||
final focusedRealm = Get.find<NavigationStateProvider>().focusedRealm.value;
|
|
||||||
return GestureDetector(
|
|
||||||
child: MouseRegion(
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
|
||||||
switchOutCurve: Curves.fastOutSlowIn,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, animation) {
|
|
||||||
return ScaleTransition(
|
|
||||||
scale: animation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _isTryingExit
|
|
||||||
? CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
)
|
|
||||||
: _buildEntryAvatar(focusedRealm!),
|
|
||||||
),
|
|
||||||
onEnter: (_) => setState(() => _isTryingExit = true),
|
|
||||||
onExit: (_) => setState(() => _isTryingExit = false),
|
|
||||||
),
|
|
||||||
onTap: () => _unFocusRealm(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntryAvatar(Realm item) {
|
|
||||||
return Hero(
|
|
||||||
tag: Key('region-realm-avatar-${item.id}'),
|
|
||||||
child: (item.avatar?.isNotEmpty ?? false)
|
|
||||||
? AccountAvatar(content: item.avatar)
|
|
||||||
: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntry(BuildContext context, Realm item) {
|
|
||||||
const padding = EdgeInsets.symmetric(horizontal: 20, vertical: 8);
|
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return InkWell(
|
|
||||||
child: _buildEntryAvatar(item).paddingSymmetric(vertical: 8),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
leading: _buildEntryAvatar(item),
|
|
||||||
contentPadding: padding,
|
|
||||||
title: Text(item.name),
|
|
||||||
subtitle: Text(
|
|
||||||
item.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final RealmProvider realms = Get.find();
|
|
||||||
final ChannelProvider channels = Get.find();
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
final NavigationStateProvider navState = Get.find();
|
|
||||||
|
|
||||||
return Obx(
|
|
||||||
() => PageTransitionSwitcher(
|
|
||||||
transitionBuilder: (child, animation, secondaryAnimation) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: navState.focusedRealm.value == null
|
|
||||||
? widget.isCollapsed
|
|
||||||
? CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 16)),
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return Tooltip(
|
|
||||||
message: element.name,
|
|
||||||
child: _buildEntry(context, element),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return _buildEntry(context, element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
if (!widget.isCollapsed &&
|
|
||||||
(navState.focusedRealm.value!.banner?.isNotEmpty ??
|
|
||||||
false))
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: AutoCacheImage(
|
|
||||||
ServiceFinder.buildUrl(
|
|
||||||
'uc',
|
|
||||||
'/attachments/${navState.focusedRealm.value!.banner}',
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.isCollapsed)
|
|
||||||
Tooltip(
|
|
||||||
message: navState.focusedRealm.value!.name,
|
|
||||||
child: _buildRealmFocusAvatar().paddingOnly(
|
|
||||||
top: 24,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
tileColor:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
leading: _buildRealmFocusAvatar(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20, vertical: 8),
|
|
||||||
title: Text(navState.focusedRealm.value!.name),
|
|
||||||
subtitle: Text(
|
|
||||||
navState.focusedRealm.value!.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(
|
|
||||||
() => ChannelListWidget(
|
|
||||||
useReplace: true,
|
|
||||||
channels: channels.availableChannels
|
|
||||||
.where((x) =>
|
|
||||||
x.realm?.id == navState.focusedRealm.value?.id)
|
|
||||||
.toList(),
|
|
||||||
isCollapsed: widget.isCollapsed,
|
|
||||||
selfId: auth.userProfile.value!['id'],
|
|
||||||
noCategory: true,
|
|
||||||
onSelected: (_) => widget.onSelected(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,7 +29,7 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
_attachmentController.text = value.toString();
|
_attachmentController.text = value.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
widget.controller.thumbnail.value = value;
|
widget.controller.thumbnail.value = value.isEmpty ? null : value;
|
||||||
},
|
},
|
||||||
initialAttachments: const [],
|
initialAttachments: const [],
|
||||||
onRemove: (_) {},
|
onRemove: (_) {},
|
||||||
@ -91,7 +91,8 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.controller.thumbnail.value = _attachmentController.text;
|
final text = _attachmentController.text;
|
||||||
|
widget.controller.thumbnail.value = text.isEmpty ? null : text;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: Text('confirm'.tr),
|
child: Text('confirm'.tr),
|
||||||
|
Loading…
Reference in New Issue
Block a user