✨ Chat room details, invitions and members management
This commit is contained in:
parent
e2e6de965b
commit
efdddf72e4
@ -33,7 +33,7 @@
|
||||
"managedPublisher": "Managed Publishers",
|
||||
"createPublisher": "Create a Publisher",
|
||||
"createPublisherHint": "To create posts, collections, etc.",
|
||||
"editPublisher": "Edit a Publisher",
|
||||
"editPublisher": "Edit Publisher",
|
||||
"syncPublisher": "Use Account Data",
|
||||
"create": "Create",
|
||||
"edit": "Edit",
|
||||
@ -62,7 +62,7 @@
|
||||
"realms": "Realms",
|
||||
"createRealm": "Create a Realm",
|
||||
"createRealmHint": "Meet friends with same interests, build communities, and more.",
|
||||
"editRealm": "Edit a Realm",
|
||||
"editRealm": "Edit Realm",
|
||||
"deleteRealm": "Delete Realm {}",
|
||||
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
|
||||
"explore": "Explore",
|
||||
@ -72,9 +72,20 @@
|
||||
"slug": "Slug",
|
||||
"slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
|
||||
"createChatRoom": "Create a Room",
|
||||
"editChatRoom": "Edit a Room",
|
||||
"editChatRoom": "Edit Room",
|
||||
"deleteChatRoom": "Delete Room",
|
||||
"chat": "Chat",
|
||||
"chatMessageHint": "Message in {}",
|
||||
"chatDirectMessageHint": "Message to {}",
|
||||
"loading": "Loading..."
|
||||
"loading": "Loading...",
|
||||
"descriptionNone": "No description yet.",
|
||||
"invites": "Invites",
|
||||
"invitesEmpty": "No invites yet, such a lonely person...",
|
||||
"chatMembers": {
|
||||
"one": "{} member",
|
||||
"other": "{} members"
|
||||
},
|
||||
"permissionOwner": "Owner",
|
||||
"permissionModerator": "Moderator",
|
||||
"permissionMember": "Member"
|
||||
}
|
||||
|
@ -7,30 +7,13 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class MessageRepository {
|
||||
final SnChat room;
|
||||
final SnChatMember identity;
|
||||
final Dio _apiClient;
|
||||
final AppDatabase _database;
|
||||
|
||||
SnChatMember? _identity;
|
||||
|
||||
final Map<String, LocalChatMessage> pendingMessages = {};
|
||||
|
||||
MessageRepository(this.room, this._apiClient, this._database) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
bool initialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (initialized) return;
|
||||
|
||||
try {
|
||||
final response = await _apiClient.get('/chat/${room.id}/members/me');
|
||||
_identity = SnChatMember.fromJson(response.data);
|
||||
initialized = true;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
MessageRepository(this.room, this.identity, this._apiClient, this._database);
|
||||
|
||||
Future<List<LocalChatMessage>> listMessages({
|
||||
int offset = 0,
|
||||
@ -143,12 +126,6 @@ class MessageRepository {
|
||||
List<SnCloudFile>? attachments,
|
||||
Map<String, dynamic>? meta,
|
||||
}) async {
|
||||
if (!initialized || _identity == null) {
|
||||
throw UnsupportedError(
|
||||
"The message repository is not ready for send message.",
|
||||
);
|
||||
}
|
||||
|
||||
// Generate a unique nonce for this message
|
||||
final nonce = const Uuid().v4();
|
||||
|
||||
@ -156,12 +133,12 @@ class MessageRepository {
|
||||
final mockMessage = SnChatMessage(
|
||||
id: 'pending_$nonce',
|
||||
chatRoomId: roomId,
|
||||
senderId: _identity!.id,
|
||||
senderId: identity.id,
|
||||
content: content,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
nonce: nonce,
|
||||
sender: _identity!,
|
||||
sender: identity,
|
||||
);
|
||||
|
||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||
|
@ -81,6 +81,7 @@ abstract class SnChatMember with _$SnChatMember {
|
||||
required DateTime? deletedAt,
|
||||
required String id,
|
||||
required int chatRoomId,
|
||||
required SnChat? chatRoom,
|
||||
required int accountId,
|
||||
required SnAccount account,
|
||||
required String? nick,
|
||||
|
@ -709,7 +709,7 @@ $SnChatMemberCopyWith<$Res> get sender {
|
||||
/// @nodoc
|
||||
mixin _$SnChatMember {
|
||||
|
||||
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; int get chatRoomId; int get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot;
|
||||
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; int get chatRoomId; SnChat? get chatRoom; int get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot;
|
||||
/// Create a copy of SnChatMember
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -722,16 +722,16 @@ $SnChatMemberCopyWith<SnChatMember> get copyWith => _$SnChatMemberCopyWithImpl<S
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,accountId,account,nick,role,notify,joinedAt,isBot);
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot)';
|
||||
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot)';
|
||||
}
|
||||
|
||||
|
||||
@ -742,11 +742,11 @@ abstract mixin class $SnChatMemberCopyWith<$Res> {
|
||||
factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, SnChat? chatRoom, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot
|
||||
});
|
||||
|
||||
|
||||
$SnAccountCopyWith<$Res> get account;
|
||||
$SnChatCopyWith<$Res>? get chatRoom;$SnAccountCopyWith<$Res> get account;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@ -759,14 +759,15 @@ class _$SnChatMemberCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnChatMember
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable
|
||||
as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,chatRoom: freezed == chatRoom ? _self.chatRoom : chatRoom // ignore: cast_nullable_to_non_nullable
|
||||
as SnChat?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
|
||||
as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
|
||||
@ -780,6 +781,18 @@ as bool,
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnChatCopyWith<$Res>? get chatRoom {
|
||||
if (_self.chatRoom == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnChatCopyWith<$Res>(_self.chatRoom!, (value) {
|
||||
return _then(_self.copyWith(chatRoom: value));
|
||||
});
|
||||
}/// Create a copy of SnChatMember
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res> get account {
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account, (value) {
|
||||
@ -793,7 +806,7 @@ $SnAccountCopyWith<$Res> get account {
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnChatMember implements SnChatMember {
|
||||
const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.isBot});
|
||||
const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.chatRoom, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.isBot});
|
||||
factory _SnChatMember.fromJson(Map<String, dynamic> json) => _$SnChatMemberFromJson(json);
|
||||
|
||||
@override final DateTime createdAt;
|
||||
@ -801,6 +814,7 @@ class _SnChatMember implements SnChatMember {
|
||||
@override final DateTime? deletedAt;
|
||||
@override final String id;
|
||||
@override final int chatRoomId;
|
||||
@override final SnChat? chatRoom;
|
||||
@override final int accountId;
|
||||
@override final SnAccount account;
|
||||
@override final String? nick;
|
||||
@ -822,16 +836,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,accountId,account,nick,role,notify,joinedAt,isBot);
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot)';
|
||||
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot)';
|
||||
}
|
||||
|
||||
|
||||
@ -842,11 +856,11 @@ abstract mixin class _$SnChatMemberCopyWith<$Res> implements $SnChatMemberCopyWi
|
||||
factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, SnChat? chatRoom, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot
|
||||
});
|
||||
|
||||
|
||||
@override $SnAccountCopyWith<$Res> get account;
|
||||
@override $SnChatCopyWith<$Res>? get chatRoom;@override $SnAccountCopyWith<$Res> get account;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@ -859,14 +873,15 @@ class __$SnChatMemberCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnChatMember
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,}) {
|
||||
return _then(_SnChatMember(
|
||||
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable
|
||||
as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,chatRoom: freezed == chatRoom ? _self.chatRoom : chatRoom // ignore: cast_nullable_to_non_nullable
|
||||
as SnChat?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
|
||||
as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
|
||||
@ -881,6 +896,18 @@ as bool,
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnChatCopyWith<$Res>? get chatRoom {
|
||||
if (_self.chatRoom == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnChatCopyWith<$Res>(_self.chatRoom!, (value) {
|
||||
return _then(_self.copyWith(chatRoom: value));
|
||||
});
|
||||
}/// Create a copy of SnChatMember
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res> get account {
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account, (value) {
|
||||
|
@ -163,6 +163,10 @@ _SnChatMember _$SnChatMemberFromJson(Map<String, dynamic> json) =>
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
id: json['id'] as String,
|
||||
chatRoomId: (json['chat_room_id'] as num).toInt(),
|
||||
chatRoom:
|
||||
json['chat_room'] == null
|
||||
? null
|
||||
: SnChat.fromJson(json['chat_room'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||
nick: json['nick'] as String?,
|
||||
@ -182,6 +186,7 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'id': instance.id,
|
||||
'chat_room_id': instance.chatRoomId,
|
||||
'chat_room': instance.chatRoom?.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
'account': instance.account.toJson(),
|
||||
'nick': instance.nick,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/user.dart';
|
||||
@ -21,6 +23,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
final user = SnAccount.fromJson(response.data);
|
||||
state = AsyncValue.data(user);
|
||||
} catch (error, stackTrace) {
|
||||
log("[UserInfo] Failed to fetch user info: $error");
|
||||
state = AsyncValue.error(error, stackTrace);
|
||||
}
|
||||
}
|
||||
@ -29,6 +32,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
state = const AsyncValue.data(null);
|
||||
final prefs = _ref.read(sharedPreferencesProvider);
|
||||
await prefs.remove(kTokenPairStoreKey);
|
||||
_ref.refresh(userInfoProvider.notifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@ -8,6 +11,7 @@ import 'package:web_socket_channel/io.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
part 'websocket.freezed.dart';
|
||||
part 'websocket.g.dart';
|
||||
|
||||
@freezed
|
||||
class WebSocketState with _$WebSocketState {
|
||||
@ -17,15 +21,35 @@ class WebSocketState with _$WebSocketState {
|
||||
const factory WebSocketState.error(String message) = _Error;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class WebSocketPacket with _$WebSocketPacket {
|
||||
const factory WebSocketPacket({
|
||||
required String type,
|
||||
required Map<String, dynamic>? data,
|
||||
required String? errorMessage,
|
||||
}) = _WebSocketPacket;
|
||||
|
||||
factory WebSocketPacket.fromJson(Map<String, dynamic> json) =>
|
||||
_$WebSocketPacketFromJson(json);
|
||||
}
|
||||
|
||||
final websocketProvider = Provider<WebSocketService>((ref) {
|
||||
return WebSocketService();
|
||||
});
|
||||
|
||||
class WebSocketService {
|
||||
WebSocketChannel? _channel;
|
||||
Stream<dynamic>? _broadcastStream;
|
||||
final StreamController<WebSocketPacket> _streamController =
|
||||
StreamController<WebSocketPacket>.broadcast();
|
||||
String? _lastUrl;
|
||||
String? _lastAtk;
|
||||
Timer? _reconnectTimer;
|
||||
|
||||
Stream<WebSocketPacket> get dataStream => _streamController.stream;
|
||||
|
||||
Future<void> connect(String url, String atk) async {
|
||||
_lastUrl = url;
|
||||
_lastAtk = atk;
|
||||
log('[WebSocket] Trying connecting to $url');
|
||||
try {
|
||||
_channel = IOWebSocketChannel.connect(
|
||||
@ -33,20 +57,48 @@ class WebSocketService {
|
||||
headers: {'Authorization': 'Bearer $atk'},
|
||||
);
|
||||
await _channel!.ready;
|
||||
_broadcastStream = _channel!.stream.asBroadcastStream();
|
||||
_channel!.stream.listen(
|
||||
(data) {
|
||||
final dataStr =
|
||||
data is Uint8List ? utf8.decode(data) : data.toString();
|
||||
final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
|
||||
_streamController.sink.add(packet);
|
||||
log("[WebSocket] Received packet: ${packet.type}");
|
||||
},
|
||||
onDone: () {
|
||||
log('[WebSocket] Connection closed, attempting to reconnect...');
|
||||
_scheduleReconnect();
|
||||
},
|
||||
onError: (error) {
|
||||
log('[WebSocket] Error occurred: $error, attempting to reconnect...');
|
||||
_scheduleReconnect();
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
log('[WebSocket] Failed to connect: $err');
|
||||
_scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
if (_lastUrl != null && _lastAtk != null) {
|
||||
connect(_lastUrl!, _lastAtk!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
WebSocketChannel? get ws => _channel;
|
||||
Stream<dynamic> get stream => _broadcastStream!;
|
||||
|
||||
void sendMessage(String message) {
|
||||
_channel!.sink.add(message);
|
||||
}
|
||||
|
||||
void close() {
|
||||
_reconnectTimer?.cancel();
|
||||
_lastUrl = null;
|
||||
_lastAtk = null;
|
||||
_channel?.sink.close();
|
||||
}
|
||||
}
|
||||
|
@ -202,6 +202,153 @@ as String,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$WebSocketPacket {
|
||||
|
||||
String get type; Map<String, dynamic>? get data; String? get errorMessage;
|
||||
/// Create a copy of WebSocketPacket
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$WebSocketPacketCopyWith<WebSocketPacket> get copyWith => _$WebSocketPacketCopyWithImpl<WebSocketPacket>(this as WebSocketPacket, _$identity);
|
||||
|
||||
/// Serializes this WebSocketPacket to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is WebSocketPacket&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(data),errorMessage);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WebSocketPacket(type: $type, data: $data, errorMessage: $errorMessage)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $WebSocketPacketCopyWith<$Res> {
|
||||
factory $WebSocketPacketCopyWith(WebSocketPacket value, $Res Function(WebSocketPacket) _then) = _$WebSocketPacketCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String type, Map<String, dynamic>? data, String? errorMessage
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$WebSocketPacketCopyWithImpl<$Res>
|
||||
implements $WebSocketPacketCopyWith<$Res> {
|
||||
_$WebSocketPacketCopyWithImpl(this._self, this._then);
|
||||
|
||||
final WebSocketPacket _self;
|
||||
final $Res Function(WebSocketPacket) _then;
|
||||
|
||||
/// Create a copy of WebSocketPacket
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? data = freezed,Object? errorMessage = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _WebSocketPacket implements WebSocketPacket {
|
||||
const _WebSocketPacket({required this.type, required final Map<String, dynamic>? data, required this.errorMessage}): _data = data;
|
||||
factory _WebSocketPacket.fromJson(Map<String, dynamic> json) => _$WebSocketPacketFromJson(json);
|
||||
|
||||
@override final String type;
|
||||
final Map<String, dynamic>? _data;
|
||||
@override Map<String, dynamic>? get data {
|
||||
final value = _data;
|
||||
if (value == null) return null;
|
||||
if (_data is EqualUnmodifiableMapView) return _data;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final String? errorMessage;
|
||||
|
||||
/// Create a copy of WebSocketPacket
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$WebSocketPacketCopyWith<_WebSocketPacket> get copyWith => __$WebSocketPacketCopyWithImpl<_WebSocketPacket>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$WebSocketPacketToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebSocketPacket&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._data, _data)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_data),errorMessage);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WebSocketPacket(type: $type, data: $data, errorMessage: $errorMessage)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$WebSocketPacketCopyWith<$Res> implements $WebSocketPacketCopyWith<$Res> {
|
||||
factory _$WebSocketPacketCopyWith(_WebSocketPacket value, $Res Function(_WebSocketPacket) _then) = __$WebSocketPacketCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, Map<String, dynamic>? data, String? errorMessage
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$WebSocketPacketCopyWithImpl<$Res>
|
||||
implements _$WebSocketPacketCopyWith<$Res> {
|
||||
__$WebSocketPacketCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _WebSocketPacket _self;
|
||||
final $Res Function(_WebSocketPacket) _then;
|
||||
|
||||
/// Create a copy of WebSocketPacket
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? data = freezed,Object? errorMessage = freezed,}) {
|
||||
return _then(_WebSocketPacket(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
21
lib/pods/websocket.g.dart
Normal file
21
lib/pods/websocket.g.dart
Normal file
@ -0,0 +1,21 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'websocket.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_WebSocketPacket _$WebSocketPacketFromJson(Map<String, dynamic> json) =>
|
||||
_WebSocketPacket(
|
||||
type: json['type'] as String,
|
||||
data: json['data'] as Map<String, dynamic>?,
|
||||
errorMessage: json['error_message'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WebSocketPacketToJson(_WebSocketPacket instance) =>
|
||||
<String, dynamic>{
|
||||
'type': instance.type,
|
||||
'data': instance.data,
|
||||
'error_message': instance.errorMessage,
|
||||
};
|
@ -37,5 +37,6 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: NewChatRoute.page, path: '/chat/new'),
|
||||
AutoRoute(page: EditChatRoute.page, path: '/chat/:id/edit'),
|
||||
AutoRoute(page: ChatRoomRoute.page, path: '/chat/:id'),
|
||||
AutoRoute(page: ChatDetailRoute.page, path: '/chat/:id/detail'),
|
||||
];
|
||||
}
|
||||
|
@ -9,32 +9,33 @@
|
||||
// coverage:ignore-file
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:auto_route/auto_route.dart' as _i14;
|
||||
import 'package:flutter/material.dart' as _i15;
|
||||
import 'package:island/models/post.dart' as _i16;
|
||||
import 'package:auto_route/auto_route.dart' as _i15;
|
||||
import 'package:flutter/material.dart' as _i16;
|
||||
import 'package:island/models/post.dart' as _i17;
|
||||
import 'package:island/screens/account.dart' as _i1;
|
||||
import 'package:island/screens/account/me.dart' as _i9;
|
||||
import 'package:island/screens/account/me/publishers.dart' as _i5;
|
||||
import 'package:island/screens/account/me/update.dart' as _i13;
|
||||
import 'package:island/screens/auth/create_account.dart' as _i4;
|
||||
import 'package:island/screens/auth/login.dart' as _i8;
|
||||
import 'package:island/screens/auth/tabs.dart' as _i12;
|
||||
import 'package:island/screens/chat/chat.dart' as _i2;
|
||||
import 'package:island/screens/chat/room.dart' as _i3;
|
||||
import 'package:island/screens/explore.dart' as _i7;
|
||||
import 'package:island/screens/posts/compose.dart' as _i10;
|
||||
import 'package:island/screens/posts/detail.dart' as _i11;
|
||||
import 'package:island/screens/realm/realms.dart' as _i6;
|
||||
import 'package:island/screens/account/me.dart' as _i10;
|
||||
import 'package:island/screens/account/me/publishers.dart' as _i6;
|
||||
import 'package:island/screens/account/me/update.dart' as _i14;
|
||||
import 'package:island/screens/auth/create_account.dart' as _i5;
|
||||
import 'package:island/screens/auth/login.dart' as _i9;
|
||||
import 'package:island/screens/auth/tabs.dart' as _i13;
|
||||
import 'package:island/screens/chat/chat.dart' as _i3;
|
||||
import 'package:island/screens/chat/room.dart' as _i4;
|
||||
import 'package:island/screens/chat/room_detail.dart' as _i2;
|
||||
import 'package:island/screens/explore.dart' as _i8;
|
||||
import 'package:island/screens/posts/compose.dart' as _i11;
|
||||
import 'package:island/screens/posts/detail.dart' as _i12;
|
||||
import 'package:island/screens/realm/realms.dart' as _i7;
|
||||
|
||||
/// generated route for
|
||||
/// [_i1.AccountScreen]
|
||||
class AccountRoute extends _i14.PageRouteInfo<void> {
|
||||
const AccountRoute({List<_i14.PageRouteInfo>? children})
|
||||
class AccountRoute extends _i15.PageRouteInfo<void> {
|
||||
const AccountRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(AccountRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'AccountRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i1.AccountScreen();
|
||||
@ -43,28 +44,69 @@ class AccountRoute extends _i14.PageRouteInfo<void> {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.ChatListScreen]
|
||||
class ChatListRoute extends _i14.PageRouteInfo<void> {
|
||||
const ChatListRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i2.ChatDetailScreen]
|
||||
class ChatDetailRoute extends _i15.PageRouteInfo<ChatDetailRouteArgs> {
|
||||
ChatDetailRoute({
|
||||
_i16.Key? key,
|
||||
required int id,
|
||||
List<_i15.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
ChatDetailRoute.name,
|
||||
args: ChatDetailRouteArgs(key: key, id: id),
|
||||
rawPathParams: {'id': id},
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'ChatDetailRoute';
|
||||
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<ChatDetailRouteArgs>(
|
||||
orElse: () => ChatDetailRouteArgs(id: pathParams.getInt('id')),
|
||||
);
|
||||
return _i2.ChatDetailScreen(key: args.key, id: args.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class ChatDetailRouteArgs {
|
||||
const ChatDetailRouteArgs({this.key, required this.id});
|
||||
|
||||
final _i16.Key? key;
|
||||
|
||||
final int id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatDetailRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i3.ChatListScreen]
|
||||
class ChatListRoute extends _i15.PageRouteInfo<void> {
|
||||
const ChatListRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(ChatListRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'ChatListRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i2.ChatListScreen();
|
||||
return const _i3.ChatListScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i3.ChatRoomScreen]
|
||||
class ChatRoomRoute extends _i14.PageRouteInfo<ChatRoomRouteArgs> {
|
||||
/// [_i4.ChatRoomScreen]
|
||||
class ChatRoomRoute extends _i15.PageRouteInfo<ChatRoomRouteArgs> {
|
||||
ChatRoomRoute({
|
||||
_i15.Key? key,
|
||||
_i16.Key? key,
|
||||
required int id,
|
||||
List<_i14.PageRouteInfo>? children,
|
||||
List<_i15.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
ChatRoomRoute.name,
|
||||
args: ChatRoomRouteArgs(key: key, id: id),
|
||||
@ -74,14 +116,14 @@ class ChatRoomRoute extends _i14.PageRouteInfo<ChatRoomRouteArgs> {
|
||||
|
||||
static const String name = 'ChatRoomRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<ChatRoomRouteArgs>(
|
||||
orElse: () => ChatRoomRouteArgs(id: pathParams.getInt('id')),
|
||||
);
|
||||
return _i3.ChatRoomScreen(key: args.key, id: args.id);
|
||||
return _i4.ChatRoomScreen(key: args.key, id: args.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -89,7 +131,7 @@ class ChatRoomRoute extends _i14.PageRouteInfo<ChatRoomRouteArgs> {
|
||||
class ChatRoomRouteArgs {
|
||||
const ChatRoomRouteArgs({this.key, required this.id});
|
||||
|
||||
final _i15.Key? key;
|
||||
final _i16.Key? key;
|
||||
|
||||
final int id;
|
||||
|
||||
@ -100,25 +142,25 @@ class ChatRoomRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i4.CreateAccountScreen]
|
||||
class CreateAccountRoute extends _i14.PageRouteInfo<void> {
|
||||
const CreateAccountRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i5.CreateAccountScreen]
|
||||
class CreateAccountRoute extends _i15.PageRouteInfo<void> {
|
||||
const CreateAccountRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(CreateAccountRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'CreateAccountRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i4.CreateAccountScreen();
|
||||
return const _i5.CreateAccountScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.EditChatScreen]
|
||||
class EditChatRoute extends _i14.PageRouteInfo<EditChatRouteArgs> {
|
||||
EditChatRoute({_i15.Key? key, int? id, List<_i14.PageRouteInfo>? children})
|
||||
/// [_i3.EditChatScreen]
|
||||
class EditChatRoute extends _i15.PageRouteInfo<EditChatRouteArgs> {
|
||||
EditChatRoute({_i16.Key? key, int? id, List<_i15.PageRouteInfo>? children})
|
||||
: super(
|
||||
EditChatRoute.name,
|
||||
args: EditChatRouteArgs(key: key, id: id),
|
||||
@ -128,14 +170,14 @@ class EditChatRoute extends _i14.PageRouteInfo<EditChatRouteArgs> {
|
||||
|
||||
static const String name = 'EditChatRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<EditChatRouteArgs>(
|
||||
orElse: () => EditChatRouteArgs(id: pathParams.optInt('id')),
|
||||
);
|
||||
return _i2.EditChatScreen(key: args.key, id: args.id);
|
||||
return _i3.EditChatScreen(key: args.key, id: args.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -143,7 +185,7 @@ class EditChatRoute extends _i14.PageRouteInfo<EditChatRouteArgs> {
|
||||
class EditChatRouteArgs {
|
||||
const EditChatRouteArgs({this.key, this.id});
|
||||
|
||||
final _i15.Key? key;
|
||||
final _i16.Key? key;
|
||||
|
||||
final int? id;
|
||||
|
||||
@ -154,12 +196,12 @@ class EditChatRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i5.EditPublisherScreen]
|
||||
class EditPublisherRoute extends _i14.PageRouteInfo<EditPublisherRouteArgs> {
|
||||
/// [_i6.EditPublisherScreen]
|
||||
class EditPublisherRoute extends _i15.PageRouteInfo<EditPublisherRouteArgs> {
|
||||
EditPublisherRoute({
|
||||
_i15.Key? key,
|
||||
_i16.Key? key,
|
||||
String? name,
|
||||
List<_i14.PageRouteInfo>? children,
|
||||
List<_i15.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
EditPublisherRoute.name,
|
||||
args: EditPublisherRouteArgs(key: key, name: name),
|
||||
@ -169,14 +211,14 @@ class EditPublisherRoute extends _i14.PageRouteInfo<EditPublisherRouteArgs> {
|
||||
|
||||
static const String name = 'EditPublisherRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<EditPublisherRouteArgs>(
|
||||
orElse: () => EditPublisherRouteArgs(name: pathParams.optString('id')),
|
||||
);
|
||||
return _i5.EditPublisherScreen(key: args.key, name: args.name);
|
||||
return _i6.EditPublisherScreen(key: args.key, name: args.name);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -184,7 +226,7 @@ class EditPublisherRoute extends _i14.PageRouteInfo<EditPublisherRouteArgs> {
|
||||
class EditPublisherRouteArgs {
|
||||
const EditPublisherRouteArgs({this.key, this.name});
|
||||
|
||||
final _i15.Key? key;
|
||||
final _i16.Key? key;
|
||||
|
||||
final String? name;
|
||||
|
||||
@ -195,12 +237,12 @@ class EditPublisherRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i6.EditRealmScreen]
|
||||
class EditRealmRoute extends _i14.PageRouteInfo<EditRealmRouteArgs> {
|
||||
/// [_i7.EditRealmScreen]
|
||||
class EditRealmRoute extends _i15.PageRouteInfo<EditRealmRouteArgs> {
|
||||
EditRealmRoute({
|
||||
_i15.Key? key,
|
||||
_i16.Key? key,
|
||||
String? slug,
|
||||
List<_i14.PageRouteInfo>? children,
|
||||
List<_i15.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
EditRealmRoute.name,
|
||||
args: EditRealmRouteArgs(key: key, slug: slug),
|
||||
@ -210,14 +252,14 @@ class EditRealmRoute extends _i14.PageRouteInfo<EditRealmRouteArgs> {
|
||||
|
||||
static const String name = 'EditRealmRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<EditRealmRouteArgs>(
|
||||
orElse: () => EditRealmRouteArgs(slug: pathParams.optString('slug')),
|
||||
);
|
||||
return _i6.EditRealmScreen(key: args.key, slug: args.slug);
|
||||
return _i7.EditRealmScreen(key: args.key, slug: args.slug);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -225,7 +267,7 @@ class EditRealmRoute extends _i14.PageRouteInfo<EditRealmRouteArgs> {
|
||||
class EditRealmRouteArgs {
|
||||
const EditRealmRouteArgs({this.key, this.slug});
|
||||
|
||||
final _i15.Key? key;
|
||||
final _i16.Key? key;
|
||||
|
||||
final String? slug;
|
||||
|
||||
@ -236,124 +278,124 @@ class EditRealmRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i7.ExploreScreen]
|
||||
class ExploreRoute extends _i14.PageRouteInfo<void> {
|
||||
const ExploreRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i8.ExploreScreen]
|
||||
class ExploreRoute extends _i15.PageRouteInfo<void> {
|
||||
const ExploreRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(ExploreRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'ExploreRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i7.ExploreScreen();
|
||||
return const _i8.ExploreScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i8.LoginScreen]
|
||||
class LoginRoute extends _i14.PageRouteInfo<void> {
|
||||
const LoginRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i9.LoginScreen]
|
||||
class LoginRoute extends _i15.PageRouteInfo<void> {
|
||||
const LoginRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(LoginRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'LoginRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i8.LoginScreen();
|
||||
return const _i9.LoginScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i5.ManagedPublisherScreen]
|
||||
class ManagedPublisherRoute extends _i14.PageRouteInfo<void> {
|
||||
const ManagedPublisherRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i6.ManagedPublisherScreen]
|
||||
class ManagedPublisherRoute extends _i15.PageRouteInfo<void> {
|
||||
const ManagedPublisherRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(ManagedPublisherRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'ManagedPublisherRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i5.ManagedPublisherScreen();
|
||||
return const _i6.ManagedPublisherScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i9.MyselfProfileScreen]
|
||||
class MyselfProfileRoute extends _i14.PageRouteInfo<void> {
|
||||
const MyselfProfileRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i10.MyselfProfileScreen]
|
||||
class MyselfProfileRoute extends _i15.PageRouteInfo<void> {
|
||||
const MyselfProfileRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(MyselfProfileRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'MyselfProfileRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i9.MyselfProfileScreen();
|
||||
return const _i10.MyselfProfileScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.NewChatScreen]
|
||||
class NewChatRoute extends _i14.PageRouteInfo<void> {
|
||||
const NewChatRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i3.NewChatScreen]
|
||||
class NewChatRoute extends _i15.PageRouteInfo<void> {
|
||||
const NewChatRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(NewChatRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'NewChatRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i2.NewChatScreen();
|
||||
return const _i3.NewChatScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i5.NewPublisherScreen]
|
||||
class NewPublisherRoute extends _i14.PageRouteInfo<void> {
|
||||
const NewPublisherRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i6.NewPublisherScreen]
|
||||
class NewPublisherRoute extends _i15.PageRouteInfo<void> {
|
||||
const NewPublisherRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(NewPublisherRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'NewPublisherRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i5.NewPublisherScreen();
|
||||
return const _i6.NewPublisherScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i6.NewRealmScreen]
|
||||
class NewRealmRoute extends _i14.PageRouteInfo<void> {
|
||||
const NewRealmRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i7.NewRealmScreen]
|
||||
class NewRealmRoute extends _i15.PageRouteInfo<void> {
|
||||
const NewRealmRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(NewRealmRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'NewRealmRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i6.NewRealmScreen();
|
||||
return const _i7.NewRealmScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i10.PostComposeScreen]
|
||||
class PostComposeRoute extends _i14.PageRouteInfo<PostComposeRouteArgs> {
|
||||
/// [_i11.PostComposeScreen]
|
||||
class PostComposeRoute extends _i15.PageRouteInfo<PostComposeRouteArgs> {
|
||||
PostComposeRoute({
|
||||
_i15.Key? key,
|
||||
_i16.SnPost? originalPost,
|
||||
List<_i14.PageRouteInfo>? children,
|
||||
_i16.Key? key,
|
||||
_i17.SnPost? originalPost,
|
||||
List<_i15.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
PostComposeRoute.name,
|
||||
args: PostComposeRouteArgs(key: key, originalPost: originalPost),
|
||||
@ -362,13 +404,13 @@ class PostComposeRoute extends _i14.PageRouteInfo<PostComposeRouteArgs> {
|
||||
|
||||
static const String name = 'PostComposeRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<PostComposeRouteArgs>(
|
||||
orElse: () => const PostComposeRouteArgs(),
|
||||
);
|
||||
return _i10.PostComposeScreen(
|
||||
return _i11.PostComposeScreen(
|
||||
key: args.key,
|
||||
originalPost: args.originalPost,
|
||||
);
|
||||
@ -379,9 +421,9 @@ class PostComposeRoute extends _i14.PageRouteInfo<PostComposeRouteArgs> {
|
||||
class PostComposeRouteArgs {
|
||||
const PostComposeRouteArgs({this.key, this.originalPost});
|
||||
|
||||
final _i15.Key? key;
|
||||
final _i16.Key? key;
|
||||
|
||||
final _i16.SnPost? originalPost;
|
||||
final _i17.SnPost? originalPost;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -390,12 +432,12 @@ class PostComposeRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i11.PostDetailScreen]
|
||||
class PostDetailRoute extends _i14.PageRouteInfo<PostDetailRouteArgs> {
|
||||
/// [_i12.PostDetailScreen]
|
||||
class PostDetailRoute extends _i15.PageRouteInfo<PostDetailRouteArgs> {
|
||||
PostDetailRoute({
|
||||
_i15.Key? key,
|
||||
_i16.Key? key,
|
||||
required int id,
|
||||
List<_i14.PageRouteInfo>? children,
|
||||
List<_i15.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
PostDetailRoute.name,
|
||||
args: PostDetailRouteArgs(key: key, id: id),
|
||||
@ -405,14 +447,14 @@ class PostDetailRoute extends _i14.PageRouteInfo<PostDetailRouteArgs> {
|
||||
|
||||
static const String name = 'PostDetailRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<PostDetailRouteArgs>(
|
||||
orElse: () => PostDetailRouteArgs(id: pathParams.getInt('id')),
|
||||
);
|
||||
return _i11.PostDetailScreen(key: args.key, id: args.id);
|
||||
return _i12.PostDetailScreen(key: args.key, id: args.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -420,7 +462,7 @@ class PostDetailRoute extends _i14.PageRouteInfo<PostDetailRouteArgs> {
|
||||
class PostDetailRouteArgs {
|
||||
const PostDetailRouteArgs({this.key, required this.id});
|
||||
|
||||
final _i15.Key? key;
|
||||
final _i16.Key? key;
|
||||
|
||||
final int id;
|
||||
|
||||
@ -431,12 +473,12 @@ class PostDetailRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i10.PostEditScreen]
|
||||
class PostEditRoute extends _i14.PageRouteInfo<PostEditRouteArgs> {
|
||||
/// [_i11.PostEditScreen]
|
||||
class PostEditRoute extends _i15.PageRouteInfo<PostEditRouteArgs> {
|
||||
PostEditRoute({
|
||||
_i15.Key? key,
|
||||
_i16.Key? key,
|
||||
required int id,
|
||||
List<_i14.PageRouteInfo>? children,
|
||||
List<_i15.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
PostEditRoute.name,
|
||||
args: PostEditRouteArgs(key: key, id: id),
|
||||
@ -446,14 +488,14 @@ class PostEditRoute extends _i14.PageRouteInfo<PostEditRouteArgs> {
|
||||
|
||||
static const String name = 'PostEditRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<PostEditRouteArgs>(
|
||||
orElse: () => PostEditRouteArgs(id: pathParams.getInt('id')),
|
||||
);
|
||||
return _i10.PostEditScreen(key: args.key, id: args.id);
|
||||
return _i11.PostEditScreen(key: args.key, id: args.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -461,7 +503,7 @@ class PostEditRoute extends _i14.PageRouteInfo<PostEditRouteArgs> {
|
||||
class PostEditRouteArgs {
|
||||
const PostEditRouteArgs({this.key, required this.id});
|
||||
|
||||
final _i15.Key? key;
|
||||
final _i16.Key? key;
|
||||
|
||||
final int id;
|
||||
|
||||
@ -472,49 +514,49 @@ class PostEditRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i6.RealmListScreen]
|
||||
class RealmListRoute extends _i14.PageRouteInfo<void> {
|
||||
const RealmListRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i7.RealmListScreen]
|
||||
class RealmListRoute extends _i15.PageRouteInfo<void> {
|
||||
const RealmListRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(RealmListRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'RealmListRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i6.RealmListScreen();
|
||||
return const _i7.RealmListScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i12.TabsScreen]
|
||||
class TabsRoute extends _i14.PageRouteInfo<void> {
|
||||
const TabsRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i13.TabsScreen]
|
||||
class TabsRoute extends _i15.PageRouteInfo<void> {
|
||||
const TabsRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(TabsRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'TabsRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i12.TabsScreen();
|
||||
return const _i13.TabsScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i13.UpdateProfileScreen]
|
||||
class UpdateProfileRoute extends _i14.PageRouteInfo<void> {
|
||||
const UpdateProfileRoute({List<_i14.PageRouteInfo>? children})
|
||||
/// [_i14.UpdateProfileScreen]
|
||||
class UpdateProfileRoute extends _i15.PageRouteInfo<void> {
|
||||
const UpdateProfileRoute({List<_i15.PageRouteInfo>? children})
|
||||
: super(UpdateProfileRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'UpdateProfileRoute';
|
||||
|
||||
static _i14.PageInfo page = _i14.PageInfo(
|
||||
static _i15.PageInfo page = _i15.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i13.UpdateProfileScreen();
|
||||
return const _i14.UpdateProfileScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -22,7 +22,9 @@ class AccountScreen extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(userInfoProvider);
|
||||
|
||||
if (!user.hasValue) return _UnauthorizedAccountScreen();
|
||||
if (!user.hasValue || user.value == null) {
|
||||
return _UnauthorizedAccountScreen();
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('Account')),
|
||||
@ -52,7 +54,7 @@ class AccountScreen extends HookConsumerWidget {
|
||||
spacing: 16,
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
fileId: user.value!.profile.pictureId,
|
||||
fileId: user.value?.profile.pictureId,
|
||||
radius: 24,
|
||||
),
|
||||
Column(
|
||||
|
@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
@ -15,6 +16,7 @@ import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@ -36,9 +38,23 @@ class ChatListScreen extends HookConsumerWidget {
|
||||
final chats = ref.watch(chatroomsJoinedProvider);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text('chat').tr()),
|
||||
appBar: AppBar(
|
||||
title: Text('chat').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.email),
|
||||
onPressed: () {
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _ChatInvitesSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
key: Key("chat-page-fab"),
|
||||
heroTag: Key("chat-page-fab"),
|
||||
onPressed: () {
|
||||
context.pushRoute(NewChatRoute());
|
||||
},
|
||||
@ -58,7 +74,7 @@ class ChatListScreen extends HookConsumerWidget {
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
leading:
|
||||
item.picture == null
|
||||
item.pictureId == null
|
||||
? CircleAvatar(
|
||||
child: Text(item.name[0].toUpperCase()),
|
||||
)
|
||||
@ -87,6 +103,14 @@ Future<SnChat?> chatroom(Ref ref, int? identifier) async {
|
||||
return SnChat.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnChatMember?> chatroomIdentity(Ref ref, int? identifier) async {
|
||||
if (identifier == null) return null;
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/chat/$identifier/members/me');
|
||||
return SnChatMember.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class NewChatScreen extends StatelessWidget {
|
||||
const NewChatScreen({super.key});
|
||||
@ -275,3 +299,146 @@ class EditChatScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SnChatMember>> chatroomInvites(Ref ref) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/chat/invites');
|
||||
return resp.data
|
||||
.map((e) => SnChatMember.fromJson(e))
|
||||
.cast<SnChatMember>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
class _ChatInvitesSheet extends HookConsumerWidget {
|
||||
const _ChatInvitesSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final invites = ref.watch(chatroomInvitesProvider);
|
||||
|
||||
Future<void> acceptInvite(SnChatMember invite) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post('/chat/invites/${invite.chatRoom!.id}/accept');
|
||||
ref.invalidate(chatroomInvitesProvider);
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> declineInvite(SnChatMember invite) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post('/chat/invites/${invite.chatRoom!.id}/decline');
|
||||
ref.invalidate(chatroomInvitesProvider);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 20,
|
||||
right: 16,
|
||||
bottom: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'invites'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.refresh),
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size(36, 36),
|
||||
),
|
||||
onPressed: () {
|
||||
ref.refresh(chatroomInvitesProvider.future);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size(36, 36),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: invites.when(
|
||||
data:
|
||||
(items) =>
|
||||
items.isEmpty
|
||||
? Center(
|
||||
child:
|
||||
Text(
|
||||
'invitesEmpty',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invite = items[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: invite.chatRoom!.pictureId,
|
||||
radius: 24,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
title: Text(invite.chatRoom!.name),
|
||||
subtitle:
|
||||
Text(
|
||||
invite.role >= 100
|
||||
? 'permissionOwner'
|
||||
: invite.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
onPressed: () => acceptInvite(invite),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => declineInvite(invite),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -163,5 +163,146 @@ class _ChatroomProviderElement extends AutoDisposeFutureProviderElement<SnChat?>
|
||||
int? get identifier => (origin as ChatroomProvider).identifier;
|
||||
}
|
||||
|
||||
String _$chatroomIdentityHash() => r'b20322591279d0336f2f309729e7e0cb9809063f';
|
||||
|
||||
/// See also [chatroomIdentity].
|
||||
@ProviderFor(chatroomIdentity)
|
||||
const chatroomIdentityProvider = ChatroomIdentityFamily();
|
||||
|
||||
/// See also [chatroomIdentity].
|
||||
class ChatroomIdentityFamily extends Family<AsyncValue<SnChatMember?>> {
|
||||
/// See also [chatroomIdentity].
|
||||
const ChatroomIdentityFamily();
|
||||
|
||||
/// See also [chatroomIdentity].
|
||||
ChatroomIdentityProvider call(int? identifier) {
|
||||
return ChatroomIdentityProvider(identifier);
|
||||
}
|
||||
|
||||
@override
|
||||
ChatroomIdentityProvider getProviderOverride(
|
||||
covariant ChatroomIdentityProvider provider,
|
||||
) {
|
||||
return call(provider.identifier);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'chatroomIdentityProvider';
|
||||
}
|
||||
|
||||
/// See also [chatroomIdentity].
|
||||
class ChatroomIdentityProvider
|
||||
extends AutoDisposeFutureProvider<SnChatMember?> {
|
||||
/// See also [chatroomIdentity].
|
||||
ChatroomIdentityProvider(int? identifier)
|
||||
: this._internal(
|
||||
(ref) => chatroomIdentity(ref as ChatroomIdentityRef, identifier),
|
||||
from: chatroomIdentityProvider,
|
||||
name: r'chatroomIdentityProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatroomIdentityHash,
|
||||
dependencies: ChatroomIdentityFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ChatroomIdentityFamily._allTransitiveDependencies,
|
||||
identifier: identifier,
|
||||
);
|
||||
|
||||
ChatroomIdentityProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.identifier,
|
||||
}) : super.internal();
|
||||
|
||||
final int? identifier;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnChatMember?> Function(ChatroomIdentityRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ChatroomIdentityProvider._internal(
|
||||
(ref) => create(ref as ChatroomIdentityRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
identifier: identifier,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnChatMember?> createElement() {
|
||||
return _ChatroomIdentityProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ChatroomIdentityProvider && other.identifier == identifier;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, identifier.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ChatroomIdentityRef on AutoDisposeFutureProviderRef<SnChatMember?> {
|
||||
/// The parameter `identifier` of this provider.
|
||||
int? get identifier;
|
||||
}
|
||||
|
||||
class _ChatroomIdentityProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnChatMember?>
|
||||
with ChatroomIdentityRef {
|
||||
_ChatroomIdentityProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
int? get identifier => (origin as ChatroomIdentityProvider).identifier;
|
||||
}
|
||||
|
||||
String _$chatroomInvitesHash() => r'c15f06c1e9c6074e6159d9d1f4404f31250ce523';
|
||||
|
||||
/// See also [chatroomInvites].
|
||||
@ProviderFor(chatroomInvites)
|
||||
final chatroomInvitesProvider =
|
||||
AutoDisposeFutureProvider<List<SnChatMember>>.internal(
|
||||
chatroomInvites,
|
||||
name: r'chatroomInvitesProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatroomInvitesHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ChatroomInvitesRef = AutoDisposeFutureProviderRef<List<SnChatMember>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:auto_route/annotations.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -9,6 +9,7 @@ import 'package:island/database/message.dart';
|
||||
import 'package:island/database/message_repository.dart';
|
||||
import 'package:island/pods/message.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
@ -19,9 +20,10 @@ import 'chat.dart';
|
||||
final messageRepositoryProvider = FutureProvider.family<MessageRepository, int>(
|
||||
(ref, roomId) async {
|
||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final database = ref.watch(databaseProvider);
|
||||
return MessageRepository(room!, apiClient, database);
|
||||
return MessageRepository(room!, identity!, apiClient, database);
|
||||
},
|
||||
);
|
||||
|
||||
@ -153,8 +155,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final chatRoom = ref.watch(chatroomProvider(id));
|
||||
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
|
||||
final messages = ref.watch(messagesProvider(id));
|
||||
final messagesNotifier = ref.read(messagesProvider(id).notifier);
|
||||
final messagesRepo = ref.watch(messageRepositoryProvider(id));
|
||||
|
||||
final messageController = useTextEditingController();
|
||||
final scrollController = useScrollController();
|
||||
@ -185,7 +189,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
height: 26,
|
||||
width: 26,
|
||||
child:
|
||||
room?.picture != null
|
||||
room?.pictureId != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room?.pictureId,
|
||||
fallbackIcon: Symbols.chat,
|
||||
@ -197,14 +201,19 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(room?.name ?? 'unknown').fontSize(19).tr(),
|
||||
Text(room?.name ?? 'unknown'.tr()).fontSize(19),
|
||||
],
|
||||
),
|
||||
loading: () => const Text('Loading...'),
|
||||
error: (_, __) => const Text('Error'),
|
||||
),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
context.router.push(ChatDetailRoute(id: id));
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
@ -217,12 +226,27 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
messageList.isEmpty
|
||||
? Center(child: Text('No messages yet'.tr()))
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
return MessageBubble(message: message);
|
||||
return chatIdentity.when(
|
||||
skipError: true,
|
||||
data:
|
||||
(identity) => MessageBubble(
|
||||
message: message,
|
||||
isCurrentUser:
|
||||
identity?.id == message.senderId,
|
||||
),
|
||||
loading:
|
||||
() => MessageBubble(
|
||||
message: message,
|
||||
isCurrentUser: false,
|
||||
),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
@ -302,14 +326,16 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
|
||||
class MessageBubble extends StatelessWidget {
|
||||
final LocalChatMessage message;
|
||||
final bool isCurrentUser;
|
||||
|
||||
const MessageBubble({Key? key, required this.message}) : super(key: key);
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.isCurrentUser,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCurrentUser =
|
||||
message.senderId == 'current_user_id'; // Replace with actual check
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Row(
|
||||
@ -365,7 +391,11 @@ class MessageBubble extends StatelessWidget {
|
||||
),
|
||||
const Gap(8),
|
||||
if (isCurrentUser)
|
||||
const SizedBox(width: 32), // Balance with avatar on the other side
|
||||
ProfilePictureWidget(
|
||||
fileId:
|
||||
message.toRemoteMessage().sender.account.profile.pictureId,
|
||||
radius: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
382
lib/screens/chat/room_detail.dart
Normal file
382
lib/screens/chat/room_detail.dart
Normal file
@ -0,0 +1,382 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/chat/chat.dart';
|
||||
import 'package:island/widgets/account/account_picker.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'room_detail.freezed.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ChatDetailScreen extends HookConsumerWidget {
|
||||
final int id;
|
||||
const ChatDetailScreen({super.key, @PathParam("id") required this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final roomState = ref.watch(chatroomProvider(id));
|
||||
final roomIdentity = ref.watch(chatroomIdentityProvider(id));
|
||||
|
||||
final isModerator = roomIdentity.when(
|
||||
loading: () => false,
|
||||
error: (error, _) => false,
|
||||
data: (identity) => (identity?.role ?? 0) >= 50,
|
||||
);
|
||||
|
||||
const iconShadow = Shadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 5.0,
|
||||
offset: Offset(1.0, 1.0),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: roomState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('Error: $error')),
|
||||
data:
|
||||
(currentRoom) => CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 180,
|
||||
pinned: true,
|
||||
leading: PageBackButton(shadows: [iconShadow]),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background:
|
||||
currentRoom?.backgroundId != null
|
||||
? CloudImageWidget(
|
||||
fileId: currentRoom!.backgroundId!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
color:
|
||||
Theme.of(context).appBarTheme.backgroundColor,
|
||||
),
|
||||
title: Text(
|
||||
currentRoom?.name ?? 'unknown'.tr(),
|
||||
).textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.people, shadows: [iconShadow]),
|
||||
onPressed: () {
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => _ChatMemberListSheet(roomId: id),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isModerator)
|
||||
_ChatRoomActionMenu(id: id, iconShadow: iconShadow),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
currentRoom?.description ?? 'descriptionNone'.tr(),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatRoomActionMenu extends StatelessWidget {
|
||||
final int id;
|
||||
final Shadow iconShadow;
|
||||
|
||||
const _ChatRoomActionMenu({required this.id, required this.iconShadow});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton(
|
||||
icon: Icon(Icons.more_vert, shadows: [iconShadow]),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
context.router.replace(EditChatRoute(id: id));
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const Gap(12),
|
||||
const Text('editChatRoom').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, color: Colors.red),
|
||||
const Gap(12),
|
||||
const Text(
|
||||
'deleteChatRoom',
|
||||
style: TextStyle(color: Colors.red),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Delete Room'),
|
||||
content: const Text(
|
||||
'Are you sure you want to delete this room? This action cannot be undone.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onPressed: () async {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ChatRoomMemberState with _$ChatRoomMemberState {
|
||||
const factory ChatRoomMemberState({
|
||||
required List<SnChatMember> members,
|
||||
required bool isLoading,
|
||||
required int total,
|
||||
String? error,
|
||||
}) = _ChatRoomMemberState;
|
||||
}
|
||||
|
||||
final chatMemberStateProvider =
|
||||
StateNotifierProvider.family<ChatMemberNotifier, ChatRoomMemberState, int>((
|
||||
ref,
|
||||
roomId,
|
||||
) {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return ChatMemberNotifier(apiClient, roomId);
|
||||
});
|
||||
|
||||
class ChatMemberNotifier extends StateNotifier<ChatRoomMemberState> {
|
||||
final int roomId;
|
||||
final Dio _apiClient;
|
||||
|
||||
ChatMemberNotifier(this._apiClient, this.roomId)
|
||||
: super(const ChatRoomMemberState(members: [], isLoading: false, total: 0));
|
||||
|
||||
Future<void> loadMore({int offset = 0, int take = 20}) async {
|
||||
if (state.isLoading) return;
|
||||
if (state.total > 0 && state.members.length >= state.total) return;
|
||||
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/chat/$roomId/members',
|
||||
queryParameters: {'offset': offset, 'take': take},
|
||||
);
|
||||
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final List<dynamic> data = response.data;
|
||||
final members = data.map((e) => SnChatMember.fromJson(e)).toList();
|
||||
|
||||
state = state.copyWith(
|
||||
members: [...state.members, ...members],
|
||||
total: total,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString(), isLoading: false);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = const ChatRoomMemberState(members: [], isLoading: false, total: 0);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatMemberListSheet extends HookConsumerWidget {
|
||||
final int roomId;
|
||||
const _ChatMemberListSheet({required this.roomId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final memberState = ref.watch(chatMemberStateProvider(roomId));
|
||||
final memberNotifier = ref.read(chatMemberStateProvider(roomId).notifier);
|
||||
|
||||
useEffect(() {
|
||||
Future(() {
|
||||
memberNotifier.loadMore();
|
||||
});
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
Future<void> invitePerson() async {
|
||||
final result = await showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => const AccountPickerSheet(),
|
||||
);
|
||||
if (result == null) return;
|
||||
try {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
await apiClient.post(
|
||||
'/chat/invites/$roomId',
|
||||
data: {'related_user_id': result.id, 'role': 0},
|
||||
);
|
||||
memberNotifier.reset();
|
||||
await memberNotifier.loadMore();
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 20,
|
||||
right: 16,
|
||||
bottom: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'chatMembers'.plural(memberState.total),
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.person_add),
|
||||
onPressed: invitePerson,
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size(36, 36),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.refresh),
|
||||
onPressed: () {
|
||||
memberNotifier.reset();
|
||||
memberNotifier.loadMore();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size(36, 36),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child:
|
||||
memberState.error != null
|
||||
? Center(child: Text(memberState.error!))
|
||||
: ListView.builder(
|
||||
itemCount: memberState.members.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == memberState.members.length) {
|
||||
if (memberState.isLoading) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (memberState.members.length <
|
||||
memberState.total) {
|
||||
memberNotifier.loadMore(
|
||||
offset: memberState.members.length,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final member = memberState.members[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: member.account.profile.pictureId,
|
||||
),
|
||||
title: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Flexible(child: Text(member.account.nick)),
|
||||
if (member.joinedAt == null)
|
||||
const Icon(Symbols.pending_actions, size: 20),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(
|
||||
member.role >= 100
|
||||
? 'permissionOwner'
|
||||
: member.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
Text('·').bold().padding(horizontal: 6),
|
||||
Expanded(
|
||||
child: Text("@${member.account.name}"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
157
lib/screens/chat/room_detail.freezed.dart
Normal file
157
lib/screens/chat/room_detail.freezed.dart
Normal file
@ -0,0 +1,157 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'room_detail.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ChatRoomMemberState {
|
||||
|
||||
List<SnChatMember> get members; bool get isLoading; int get total; String? get error;
|
||||
/// Create a copy of ChatRoomMemberState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ChatRoomMemberStateCopyWith<ChatRoomMemberState> get copyWith => _$ChatRoomMemberStateCopyWithImpl<ChatRoomMemberState>(this as ChatRoomMemberState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatRoomMemberState&&const DeepCollectionEquality().equals(other.members, members)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.total, total) || other.total == total)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(members),isLoading,total,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatRoomMemberState(members: $members, isLoading: $isLoading, total: $total, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ChatRoomMemberStateCopyWith<$Res> {
|
||||
factory $ChatRoomMemberStateCopyWith(ChatRoomMemberState value, $Res Function(ChatRoomMemberState) _then) = _$ChatRoomMemberStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<SnChatMember> members, bool isLoading, int total, String? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ChatRoomMemberStateCopyWithImpl<$Res>
|
||||
implements $ChatRoomMemberStateCopyWith<$Res> {
|
||||
_$ChatRoomMemberStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ChatRoomMemberState _self;
|
||||
final $Res Function(ChatRoomMemberState) _then;
|
||||
|
||||
/// Create a copy of ChatRoomMemberState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? members = null,Object? isLoading = null,Object? total = null,Object? error = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
members: null == members ? _self.members : members // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnChatMember>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ChatRoomMemberState implements ChatRoomMemberState {
|
||||
const _ChatRoomMemberState({required final List<SnChatMember> members, required this.isLoading, required this.total, this.error}): _members = members;
|
||||
|
||||
|
||||
final List<SnChatMember> _members;
|
||||
@override List<SnChatMember> get members {
|
||||
if (_members is EqualUnmodifiableListView) return _members;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_members);
|
||||
}
|
||||
|
||||
@override final bool isLoading;
|
||||
@override final int total;
|
||||
@override final String? error;
|
||||
|
||||
/// Create a copy of ChatRoomMemberState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ChatRoomMemberStateCopyWith<_ChatRoomMemberState> get copyWith => __$ChatRoomMemberStateCopyWithImpl<_ChatRoomMemberState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatRoomMemberState&&const DeepCollectionEquality().equals(other._members, _members)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.total, total) || other.total == total)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_members),isLoading,total,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatRoomMemberState(members: $members, isLoading: $isLoading, total: $total, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ChatRoomMemberStateCopyWith<$Res> implements $ChatRoomMemberStateCopyWith<$Res> {
|
||||
factory _$ChatRoomMemberStateCopyWith(_ChatRoomMemberState value, $Res Function(_ChatRoomMemberState) _then) = __$ChatRoomMemberStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<SnChatMember> members, bool isLoading, int total, String? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ChatRoomMemberStateCopyWithImpl<$Res>
|
||||
implements _$ChatRoomMemberStateCopyWith<$Res> {
|
||||
__$ChatRoomMemberStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ChatRoomMemberState _self;
|
||||
final $Res Function(_ChatRoomMemberState) _then;
|
||||
|
||||
/// Create a copy of ChatRoomMemberState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? members = null,Object? isLoading = null,Object? total = null,Object? error = freezed,}) {
|
||||
return _then(_ChatRoomMemberState(
|
||||
members: null == members ? _self._members : members // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnChatMember>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
@ -21,7 +21,7 @@ class ExploreScreen extends ConsumerWidget {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('Explore')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
key: Key("explore-page-fab"),
|
||||
heroTag: Key("explore-page-fab"),
|
||||
onPressed: () {
|
||||
context.router.push(PostComposeRoute()).then((value) {
|
||||
if (value != null) {
|
||||
|
@ -39,7 +39,7 @@ class RealmListScreen extends HookConsumerWidget {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('realms').tr()),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
key: Key("realms-page-fab"),
|
||||
heroTag: Key("realms-page-fab"),
|
||||
child: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
context.router.push(NewRealmRoute());
|
||||
|
107
lib/widgets/account/account_picker.dart
Normal file
107
lib/widgets/account/account_picker.dart
Normal file
@ -0,0 +1,107 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/user.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'account_picker.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnAccount>> searchAccounts(Ref ref, {required String query}) async {
|
||||
if (query.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final response = await apiClient.get(
|
||||
'/accounts/search',
|
||||
queryParameters: {'query': query},
|
||||
);
|
||||
|
||||
return response.data!
|
||||
.map((json) => SnAccount.fromJson(json))
|
||||
.cast<SnAccount>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
class AccountPickerSheet extends HookConsumerWidget {
|
||||
const AccountPickerSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchController = useTextEditingController();
|
||||
final debounceTimer = useState<Timer?>(null);
|
||||
|
||||
void onSearchChanged(String query) {
|
||||
debounceTimer.value?.cancel();
|
||||
debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
|
||||
ref.read(searchAccountsProvider(query: query));
|
||||
});
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.4,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: onSearchChanged,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search accounts...',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 18,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final searchResult = ref.watch(
|
||||
searchAccountsProvider(query: searchController.text),
|
||||
);
|
||||
|
||||
return searchResult.when(
|
||||
data:
|
||||
(accounts) => ListView.builder(
|
||||
itemCount: accounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = accounts[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: account.profile.pictureId,
|
||||
),
|
||||
title: Text(account.nick),
|
||||
subtitle: Text('@${account.name}'),
|
||||
onTap: () => Navigator.of(context).pop(account),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading:
|
||||
() => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => Center(child: Text('Error: $error')),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
153
lib/widgets/account/account_picker.g.dart
Normal file
153
lib/widgets/account/account_picker.g.dart
Normal file
@ -0,0 +1,153 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'account_picker.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$searchAccountsHash() => r'4923cd06876d04515d95d3c58ee3ea9e05c58e4a';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [searchAccounts].
|
||||
@ProviderFor(searchAccounts)
|
||||
const searchAccountsProvider = SearchAccountsFamily();
|
||||
|
||||
/// See also [searchAccounts].
|
||||
class SearchAccountsFamily extends Family<AsyncValue<List<SnAccount>>> {
|
||||
/// See also [searchAccounts].
|
||||
const SearchAccountsFamily();
|
||||
|
||||
/// See also [searchAccounts].
|
||||
SearchAccountsProvider call({required String query}) {
|
||||
return SearchAccountsProvider(query: query);
|
||||
}
|
||||
|
||||
@override
|
||||
SearchAccountsProvider getProviderOverride(
|
||||
covariant SearchAccountsProvider provider,
|
||||
) {
|
||||
return call(query: provider.query);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'searchAccountsProvider';
|
||||
}
|
||||
|
||||
/// See also [searchAccounts].
|
||||
class SearchAccountsProvider
|
||||
extends AutoDisposeFutureProvider<List<SnAccount>> {
|
||||
/// See also [searchAccounts].
|
||||
SearchAccountsProvider({required String query})
|
||||
: this._internal(
|
||||
(ref) => searchAccounts(ref as SearchAccountsRef, query: query),
|
||||
from: searchAccountsProvider,
|
||||
name: r'searchAccountsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$searchAccountsHash,
|
||||
dependencies: SearchAccountsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
SearchAccountsFamily._allTransitiveDependencies,
|
||||
query: query,
|
||||
);
|
||||
|
||||
SearchAccountsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.query,
|
||||
}) : super.internal();
|
||||
|
||||
final String query;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<SnAccount>> Function(SearchAccountsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SearchAccountsProvider._internal(
|
||||
(ref) => create(ref as SearchAccountsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
query: query,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<SnAccount>> createElement() {
|
||||
return _SearchAccountsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SearchAccountsProvider && other.query == query;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, query.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin SearchAccountsRef on AutoDisposeFutureProviderRef<List<SnAccount>> {
|
||||
/// The parameter `query` of this provider.
|
||||
String get query;
|
||||
}
|
||||
|
||||
class _SearchAccountsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<SnAccount>>
|
||||
with SearchAccountsRef {
|
||||
_SearchAccountsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get query => (origin as SearchAccountsProvider).query;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@ -157,14 +158,21 @@ class AppScaffold extends StatelessWidget {
|
||||
}
|
||||
|
||||
class PageBackButton extends StatelessWidget {
|
||||
const PageBackButton({super.key});
|
||||
final List<Shadow>? shadows;
|
||||
const PageBackButton({super.key, this.shadows});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BackButton(
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
context.router.maybePop();
|
||||
},
|
||||
icon: Icon(
|
||||
(!kIsWeb && (Platform.isMacOS || Platform.isIOS))
|
||||
? Symbols.arrow_back_ios_new
|
||||
: Symbols.arrow_back,
|
||||
shadows: shadows,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,30 @@ class CloudFileWidget extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class CloudImageWidget extends ConsumerWidget {
|
||||
final String fileId;
|
||||
final BoxFit fit;
|
||||
final double aspectRatio;
|
||||
final String? blurHash;
|
||||
const CloudImageWidget({
|
||||
super.key,
|
||||
required this.fileId,
|
||||
this.aspectRatio = 1,
|
||||
this.fit = BoxFit.cover,
|
||||
this.blurHash,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final uri = '$serverUrl/files/$fileId';
|
||||
return AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: UniversalImage(uri: uri, blurHash: blurHash),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfilePictureWidget extends ConsumerWidget {
|
||||
final String? fileId;
|
||||
final double radius;
|
||||
|
@ -1,4 +1,4 @@
|
||||
platform :osx, '10.14'
|
||||
platform :osx, '10.15'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
@ -1,29 +1,264 @@
|
||||
PODS:
|
||||
- bitsdojo_window_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_picker (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Firebase/CoreOnly (11.10.0):
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- Firebase/Messaging (11.10.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.10.0)
|
||||
- firebase_core (3.13.0):
|
||||
- Firebase/CoreOnly (~> 11.10.0)
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (15.2.5):
|
||||
- Firebase/CoreOnly (~> 11.10.0)
|
||||
- Firebase/Messaging (~> 11.10.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- FirebaseCore (11.10.0):
|
||||
- FirebaseCoreInternal (~> 11.10.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.10.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.10.0):
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.10.0):
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Reachability (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- flutter_inappwebview_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_platform_alert (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_udid (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain
|
||||
- FlutterMacOS (1.0.0)
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Network (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/Reachability (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- irondash_engine_context (0.0.1):
|
||||
- FlutterMacOS
|
||||
- media_kit_libs_macos_video (1.0.4):
|
||||
- FlutterMacOS
|
||||
- media_kit_video (0.0.1):
|
||||
- FlutterMacOS
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- quill_native_bridge_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (3.49.1):
|
||||
- sqlite3/common (= 3.49.1)
|
||||
- sqlite3/common (3.49.1)
|
||||
- sqlite3/dbstatvtab (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/math (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/math
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- super_native_extensions (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- volume_controller (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
||||
- flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
|
||||
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- quill_native_bridge_macos (from `Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Firebase
|
||||
- FirebaseCore
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- SAMKeychain
|
||||
- sqlite3
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
bitsdojo_window_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_picker:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
firebase_core:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
|
||||
firebase_messaging:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
||||
flutter_inappwebview_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||
flutter_platform_alert:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos
|
||||
flutter_udid:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
irondash_engine_context:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
|
||||
media_kit_libs_macos_video:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
||||
media_kit_video:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
quill_native_bridge_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
sqlite3_flutter_libs:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin
|
||||
super_native_extensions:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
volume_controller:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
|
||||
firebase_core: efd50ad8177dc489af1b9163a560359cf1b30597
|
||||
firebase_messaging: acf2566068a55d7eb8cddfee5b094754070a5b88
|
||||
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
|
||||
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
|
||||
FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3
|
||||
FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
|
||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
|
||||
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
quill_native_bridge_macos: 2b005cb56902bb740e0cd9620aa399dfac6b4882
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
|
||||
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
||||
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
|
||||
|
||||
PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
@ -122,7 +122,6 @@
|
||||
97CE52E8413D7559BF9A7981 /* Pods-RunnerTests.release.xcconfig */,
|
||||
30FD87E3D579B3530B39AD6D /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -244,6 +243,7 @@
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
F1E275A871246799FC3019F6 /* [CP] Embed Pods Frameworks */,
|
||||
8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -387,6 +387,23 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
8D06F41203F1FD2FDE04DC7F /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
B2D8214A877A92B7299E734E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -583,6 +600,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
@ -715,6 +733,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -735,6 +754,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user