diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 35e9e72..6c94bcb 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -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" } diff --git a/lib/database/message_repository.dart b/lib/database/message_repository.dart index 862ca90..f6c4b71 100644 --- a/lib/database/message_repository.dart +++ b/lib/database/message_repository.dart @@ -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 pendingMessages = {}; - MessageRepository(this.room, this._apiClient, this._database) { - initialize(); - } - - bool initialized = false; - - Future 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> listMessages({ int offset = 0, @@ -143,12 +126,6 @@ class MessageRepository { List? attachments, Map? 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( diff --git a/lib/models/chat.dart b/lib/models/chat.dart index 02e9db4..c44cee9 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -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, diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index 7e723bd..2b4b50a 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -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 get copyWith => _$SnChatMemberCopyWithImpl 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 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 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) { diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index 2aa0cc7..4e32ab5 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -163,6 +163,10 @@ _SnChatMember _$SnChatMemberFromJson(Map 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), accountId: (json['account_id'] as num).toInt(), account: SnAccount.fromJson(json['account'] as Map), nick: json['nick'] as String?, @@ -182,6 +186,7 @@ Map _$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, diff --git a/lib/pods/userinfo.dart b/lib/pods/userinfo.dart index 87143df..f61c1ef 100644 --- a/lib/pods/userinfo.dart +++ b/lib/pods/userinfo.dart @@ -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> { 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> { state = const AsyncValue.data(null); final prefs = _ref.read(sharedPreferencesProvider); await prefs.remove(kTokenPairStoreKey); + _ref.refresh(userInfoProvider.notifier); } } diff --git a/lib/pods/websocket.dart b/lib/pods/websocket.dart index d305ad5..f3f172c 100644 --- a/lib/pods/websocket.dart +++ b/lib/pods/websocket.dart @@ -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? data, + required String? errorMessage, + }) = _WebSocketPacket; + + factory WebSocketPacket.fromJson(Map json) => + _$WebSocketPacketFromJson(json); +} + final websocketProvider = Provider((ref) { return WebSocketService(); }); class WebSocketService { WebSocketChannel? _channel; - Stream? _broadcastStream; + final StreamController _streamController = + StreamController.broadcast(); + String? _lastUrl; + String? _lastAtk; + Timer? _reconnectTimer; + + Stream get dataStream => _streamController.stream; Future 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 get stream => _broadcastStream!; void sendMessage(String message) { _channel!.sink.add(message); } void close() { + _reconnectTimer?.cancel(); + _lastUrl = null; + _lastAtk = null; _channel?.sink.close(); } } diff --git a/lib/pods/websocket.freezed.dart b/lib/pods/websocket.freezed.dart index 8837a1c..139372a 100644 --- a/lib/pods/websocket.freezed.dart +++ b/lib/pods/websocket.freezed.dart @@ -202,6 +202,153 @@ as String, } +} + + +/// @nodoc +mixin _$WebSocketPacket { + + String get type; Map? 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 get copyWith => _$WebSocketPacketCopyWithImpl(this as WebSocketPacket, _$identity); + + /// Serializes this WebSocketPacket to a JSON map. + Map 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? 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?,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? data, required this.errorMessage}): _data = data; + factory _WebSocketPacket.fromJson(Map json) => _$WebSocketPacketFromJson(json); + +@override final String type; + final Map? _data; +@override Map? 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 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? 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?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + } // dart format on diff --git a/lib/pods/websocket.g.dart b/lib/pods/websocket.g.dart new file mode 100644 index 0000000..0e4d804 --- /dev/null +++ b/lib/pods/websocket.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'websocket.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_WebSocketPacket _$WebSocketPacketFromJson(Map json) => + _WebSocketPacket( + type: json['type'] as String, + data: json['data'] as Map?, + errorMessage: json['error_message'] as String?, + ); + +Map _$WebSocketPacketToJson(_WebSocketPacket instance) => + { + 'type': instance.type, + 'data': instance.data, + 'error_message': instance.errorMessage, + }; diff --git a/lib/route.dart b/lib/route.dart index 3058d4d..12ae600 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -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'), ]; } diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 87d57e4..ec049b8 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -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 { - const AccountRoute({List<_i14.PageRouteInfo>? children}) +class AccountRoute extends _i15.PageRouteInfo { + 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 { } /// generated route for -/// [_i2.ChatListScreen] -class ChatListRoute extends _i14.PageRouteInfo { - const ChatListRoute({List<_i14.PageRouteInfo>? children}) +/// [_i2.ChatDetailScreen] +class ChatDetailRoute extends _i15.PageRouteInfo { + 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( + 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 { + 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 { +/// [_i4.ChatRoomScreen] +class ChatRoomRoute extends _i15.PageRouteInfo { 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 { 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( 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 { 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 { - const CreateAccountRoute({List<_i14.PageRouteInfo>? children}) +/// [_i5.CreateAccountScreen] +class CreateAccountRoute extends _i15.PageRouteInfo { + 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 { - EditChatRoute({_i15.Key? key, int? id, List<_i14.PageRouteInfo>? children}) +/// [_i3.EditChatScreen] +class EditChatRoute extends _i15.PageRouteInfo { + 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 { 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( 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 { 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 { +/// [_i6.EditPublisherScreen] +class EditPublisherRoute extends _i15.PageRouteInfo { 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 { 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( 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 { 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 { +/// [_i7.EditRealmScreen] +class EditRealmRoute extends _i15.PageRouteInfo { 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 { 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( 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 { 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 { - const ExploreRoute({List<_i14.PageRouteInfo>? children}) +/// [_i8.ExploreScreen] +class ExploreRoute extends _i15.PageRouteInfo { + 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 { - const LoginRoute({List<_i14.PageRouteInfo>? children}) +/// [_i9.LoginScreen] +class LoginRoute extends _i15.PageRouteInfo { + 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 { - const ManagedPublisherRoute({List<_i14.PageRouteInfo>? children}) +/// [_i6.ManagedPublisherScreen] +class ManagedPublisherRoute extends _i15.PageRouteInfo { + 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 { - const MyselfProfileRoute({List<_i14.PageRouteInfo>? children}) +/// [_i10.MyselfProfileScreen] +class MyselfProfileRoute extends _i15.PageRouteInfo { + 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 { - const NewChatRoute({List<_i14.PageRouteInfo>? children}) +/// [_i3.NewChatScreen] +class NewChatRoute extends _i15.PageRouteInfo { + 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 { - const NewPublisherRoute({List<_i14.PageRouteInfo>? children}) +/// [_i6.NewPublisherScreen] +class NewPublisherRoute extends _i15.PageRouteInfo { + 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 { - const NewRealmRoute({List<_i14.PageRouteInfo>? children}) +/// [_i7.NewRealmScreen] +class NewRealmRoute extends _i15.PageRouteInfo { + 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 { +/// [_i11.PostComposeScreen] +class PostComposeRoute extends _i15.PageRouteInfo { 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 { static const String name = 'PostComposeRoute'; - static _i14.PageInfo page = _i14.PageInfo( + static _i15.PageInfo page = _i15.PageInfo( name, builder: (data) { final args = data.argsAs( orElse: () => const PostComposeRouteArgs(), ); - return _i10.PostComposeScreen( + return _i11.PostComposeScreen( key: args.key, originalPost: args.originalPost, ); @@ -379,9 +421,9 @@ class PostComposeRoute extends _i14.PageRouteInfo { 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 { +/// [_i12.PostDetailScreen] +class PostDetailRoute extends _i15.PageRouteInfo { 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 { 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( 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 { 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 { +/// [_i11.PostEditScreen] +class PostEditRoute extends _i15.PageRouteInfo { 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 { 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( 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 { 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 { - const RealmListRoute({List<_i14.PageRouteInfo>? children}) +/// [_i7.RealmListScreen] +class RealmListRoute extends _i15.PageRouteInfo { + 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 { - const TabsRoute({List<_i14.PageRouteInfo>? children}) +/// [_i13.TabsScreen] +class TabsRoute extends _i15.PageRouteInfo { + 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 { - const UpdateProfileRoute({List<_i14.PageRouteInfo>? children}) +/// [_i14.UpdateProfileScreen] +class UpdateProfileRoute extends _i15.PageRouteInfo { + 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(); }, ); } diff --git a/lib/screens/account.dart b/lib/screens/account.dart index bab8584..6c457b2 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -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( diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index bc5a751..cd3d402 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -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 chatroom(Ref ref, int? identifier) async { return SnChat.fromJson(resp.data); } +@riverpod +Future 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> 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() + .toList(); +} + +class _ChatInvitesSheet extends HookConsumerWidget { + const _ChatInvitesSheet(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final invites = ref.watch(chatroomInvitesProvider); + + Future 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 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')), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/chat/chat.g.dart b/lib/screens/chat/chat.g.dart index 2e13029..fc0d647 100644 --- a/lib/screens/chat/chat.g.dart +++ b/lib/screens/chat/chat.g.dart @@ -163,5 +163,146 @@ class _ChatroomProviderElement extends AutoDisposeFutureProviderElement 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> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chatroomIdentityProvider'; +} + +/// See also [chatroomIdentity]. +class ChatroomIdentityProvider + extends AutoDisposeFutureProvider { + /// 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 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 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 { + /// The parameter `identifier` of this provider. + int? get identifier; +} + +class _ChatroomIdentityProviderElement + extends AutoDisposeFutureProviderElement + 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>.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>; // 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 diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index f95c5d3..d895373 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -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( (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, + ), ], ), ); diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart new file mode 100644 index 0000000..bc49ef9 --- /dev/null +++ b/lib/screens/chat/room_detail.dart @@ -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 members, + required bool isLoading, + required int total, + String? error, + }) = _ChatRoomMemberState; +} + +final chatMemberStateProvider = + StateNotifierProvider.family(( + ref, + roomId, + ) { + final apiClient = ref.watch(apiClientProvider); + return ChatMemberNotifier(apiClient, roomId); + }); + +class ChatMemberNotifier extends StateNotifier { + final int roomId; + final Dio _apiClient; + + ChatMemberNotifier(this._apiClient, this.roomId) + : super(const ChatRoomMemberState(members: [], isLoading: false, total: 0)); + + Future 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 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 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}"), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/chat/room_detail.freezed.dart b/lib/screens/chat/room_detail.freezed.dart new file mode 100644 index 0000000..9821362 --- /dev/null +++ b/lib/screens/chat/room_detail.freezed.dart @@ -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 value) => value; +/// @nodoc +mixin _$ChatRoomMemberState { + + List 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 get copyWith => _$ChatRoomMemberStateCopyWithImpl(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 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,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 members, required this.isLoading, required this.total, this.error}): _members = members; + + + final List _members; +@override List 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 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,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 diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index f178aa6..10aec43 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -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) { diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 75dad21..818cf9b 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -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()); diff --git a/lib/widgets/account/account_picker.dart b/lib/widgets/account/account_picker.dart new file mode 100644 index 0000000..1c34567 --- /dev/null +++ b/lib/widgets/account/account_picker.dart @@ -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> 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() + .toList(); +} + +class AccountPickerSheet extends HookConsumerWidget { + const AccountPickerSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final searchController = useTextEditingController(); + final debounceTimer = useState(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')), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/account/account_picker.g.dart b/lib/widgets/account/account_picker.g.dart new file mode 100644 index 0000000..bd26e79 --- /dev/null +++ b/lib/widgets/account/account_picker.g.dart @@ -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>> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'searchAccountsProvider'; +} + +/// See also [searchAccounts]. +class SearchAccountsProvider + extends AutoDisposeFutureProvider> { + /// 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> 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> 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> { + /// The parameter `query` of this provider. + String get query; +} + +class _SearchAccountsProviderElement + extends AutoDisposeFutureProviderElement> + 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 diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 8d68ee9..9c04ae9 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -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? 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, + ), ); } } diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index b0e0a81..1124e3f 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -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; diff --git a/macos/Podfile b/macos/Podfile index 29c8eb3..ff5ddb3 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -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' diff --git a/macos/Podfile.lock b/macos/Podfile.lock index d296772..4798e91 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -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 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 849d4fa..848ffe1 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -122,7 +122,6 @@ 97CE52E8413D7559BF9A7981 /* Pods-RunnerTests.release.xcconfig */, 30FD87E3D579B3530B39AD6D /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -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; };