Compare commits

...

17 Commits

Author SHA1 Message Date
LittleSheep
c9b71701c8 🚀 Launch 3.2.0+129 2025-08-26 01:33:57 +08:00
LittleSheep
28e98488f1 🌐 Localize new stuff 2025-08-26 01:24:58 +08:00
LittleSheep
b4d476613e 🐛 Optimzation and bug fixes 2025-08-26 01:18:54 +08:00
LittleSheep
b48a1aac44 Search messages!
♻️ Optimize messages loading, syncing
2025-08-26 01:05:30 +08:00
LittleSheep
596d212593 🐛 Fix account name localization 2025-08-26 00:17:34 +08:00
LittleSheep
54f290327e Copyable file ID 2025-08-26 00:08:50 +08:00
LittleSheep
16f248ceab 💄 Optimize file saving 2025-08-26 00:03:43 +08:00
LittleSheep
856d811187 🐛 Fix notification tap in system wide 2025-08-25 23:14:59 +08:00
LittleSheep
d07b194c04 🐛 Fixes 2025-08-25 20:44:35 +08:00
LittleSheep
2554b58be6 Show automated account in pfc 2025-08-25 20:31:43 +08:00
LittleSheep
a627b5838e 👔 Disable pin reply 2025-08-25 19:57:30 +08:00
LittleSheep
c479a9f381 Show social credits 2025-08-25 19:56:34 +08:00
LittleSheep
02057e663b Real previewing chat 2025-08-25 19:36:48 +08:00
LittleSheep
6501594100 Event dairy 2025-08-25 18:31:57 +08:00
LittleSheep
c6599edc3d 💄 Serval changes to optimize UX 2025-08-25 18:03:50 +08:00
LittleSheep
709a0620b6 Show pinned posts on realms, publishers 2025-08-25 17:09:24 +08:00
LittleSheep
f9b2a96c7c Pin post 2025-08-25 16:55:06 +08:00
46 changed files with 1547 additions and 421 deletions

View File

@@ -386,6 +386,7 @@
"postSettings": "Settings",
"postPublisherUnselected": "Publisher Unspecified",
"postType": "Post Type",
"postTypePost": "Post",
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"postVisibilityPublic": "Public",
@@ -883,6 +884,7 @@
"stellarProgram": "Stellar Program",
"socialCredits": "Social Credits",
"credits": "Credits",
"creditsStatus": "Credits Status",
"socialCreditsDescription": "Social Credit is a way for Solar Network to evaluate users. It is calculated based on their behavior and interactions. With a base score of 100, higher scores indicate a user's credibility within the community. Scores change over time to reflect a user's recent behavior. Users with higher credit ratings enjoy more benefits, while users with lower credit ratings may have some functionality restricted.",
"socialCreditsLevelPoor": "Poor",
"socialCreditsLevelNormal": "Normal",
@@ -926,5 +928,37 @@
"newSecretGenerated": "New Secret Generated",
"copySecretHint": "Please copy this secret and store it somewhere safe. You will not be able to see it again.",
"expiresIn": "Expires In (seconds)",
"isOidc": "OIDC Compliant"
"isOidc": "OIDC Compliant",
"pinPost": "Pin Post",
"unpinPost": "Unpin Post",
"pinnedPost": "Pinned",
"publisherPage": "Publisher Page",
"realmPage": "Realm Page",
"replyPage": "Reply Page",
"pinPostPublisherHint": "Pin this post to your publisher page",
"pinPostRealmHint": "Pin this post to the realm page",
"pinPostRealmDisabledHint": "This post doesn't belong to any realm",
"pinPostReplyHint": "Pin this post to the reply page",
"pinPostReplyDisabledHint": "This post is not a reply",
"pin": "Pin",
"unpinPostHint": "Are you sure you want to unpin this post?",
"all": "All",
"statusPresent": "Present",
"accountAutomated": "Automated",
"chatBreakClearButton": "Clear",
"chatBreak5m": "5m",
"chatBreak10m": "10m",
"chatBreak15m": "15m",
"chatBreak30m": "30m",
"chatBreakCustomMinutes": "Custom (minutes)",
"chatBreakEnterMinutes": "Enter minutes",
"errorGeneric": "Error: {}",
"searchMessages": "Search Messages",
"messagesCount": "{} messages",
"dotSeparator": "·",
"roleValidationHint": "Role must be between 0 and 100",
"searchMessagesHint": "Search messages...",
"searchLinks": "Links",
"searchAttachments": "Attachments",
"noMessagesFound": "No messages found"
}

View File

@@ -855,5 +855,7 @@
"newSecretGenerated": "已生成新密钥",
"copySecretHint": "请复制此密钥并将其存放在安全的地方。您将无法再次看到它。",
"expiresIn": "过期时间(秒)",
"isOidc": "OIDC 兼容"
"isOidc": "OIDC 兼容",
"statusPresent": "至今",
"accountAutomated": "机器人"
}

View File

@@ -40,6 +40,8 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0)
- Firebase/Crashlytics (12.0.0):
@@ -303,6 +305,7 @@ DEPENDENCIES:
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
@@ -381,6 +384,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
file_saver:
:path: ".symlinks/plugins/file_saver/ios"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
@@ -464,6 +469,7 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4

View File

@@ -68,6 +68,34 @@ class AppDatabase extends _$AppDatabase {
return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
}
Future<int> getTotalMessagesForRoom(String roomId) {
return (select(chatMessages)..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length);
}
Future<List<LocalChatMessage>> searchMessages(
String roomId,
String query,
) async {
var selectStatement = select(chatMessages)
..where((m) => m.roomId.equals(roomId));
if (query.isNotEmpty) {
selectStatement =
selectStatement
..where((m) => m.content.like('%${query.toLowerCase()}%'));
}
final messages =
await (selectStatement
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
.get();
return messages.map((msg) => companionToMessage(msg)).toList();
}
// Convert between Drift and model objects
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
return ChatMessagesCompanion(

View File

@@ -8,7 +8,7 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -169,12 +169,12 @@ class IslandApp extends HookConsumerWidget {
final theme = ref.watch(themeProvider);
void handleMessage(RemoteMessage notification) {
if (notification.data['action_uri'] != null) {
var uri = notification.data['action_uri'] as String;
if (notification.data['meta']?['action_uri'] != null) {
var uri = notification.data['meta']['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
final router = ref.read(routerProvider);
router.go(notification.data['action_uri']);
router.push(notification.data['meta']['action_uri']);
} else {
// External links
launchUrlString(uri);
@@ -186,27 +186,6 @@ class IslandApp extends HookConsumerWidget {
if (!kIsWeb && Platform.isLinux) {
return null;
}
const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async {
final String? link = await channel.invokeMethod('initialLink');
if (link != null) {
final router = ref.read(routerProvider);
router.go(link);
}
}
if (!kIsWeb && Platform.isAndroid) {
handleInitialLink();
}
channel.setMethodCallHandler((call) async {
if (call.method == 'newLink') {
final String link = call.arguments;
final router = ref.read(routerProvider);
router.go(link);
}
});
// When the app is opened from a terminated state.
FirebaseMessaging.instance.getInitialMessage().then((message) {

View File

@@ -71,6 +71,8 @@ sealed class SnAccountProfile with _$SnAccountProfile {
SnAccountBadge? activeBadge,
required int experience,
required int level,
@Default(100) double socialCredits,
@Default(0) int socialCreditsLevel,
required double levelingProgress,
required SnCloudFile? picture,
required SnCloudFile? background,

View File

@@ -613,7 +613,7 @@ as String,
/// @nodoc
mixin _$SnAccountProfile {
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -626,16 +626,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -646,7 +646,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -663,7 +663,7 @@ class _$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -680,6 +680,8 @@ as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : last
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable
as double,socialCreditsLevel: null == socialCreditsLevel ? _self.socialCreditsLevel : socialCreditsLevel // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
@@ -817,10 +819,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -838,10 +840,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile():
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -855,10 +857,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -870,7 +872,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id;
@@ -894,6 +896,8 @@ class _SnAccountProfile implements SnAccountProfile {
@override final SnAccountBadge? activeBadge;
@override final int experience;
@override final int level;
@override@JsonKey() final double socialCredits;
@override@JsonKey() final int socialCreditsLevel;
@override final double levelingProgress;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@@ -915,16 +919,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -935,7 +939,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -952,7 +956,7 @@ class __$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountProfile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -969,6 +973,8 @@ as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : last
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable
as double,socialCreditsLevel: null == socialCreditsLevel ? _self.socialCreditsLevel : socialCreditsLevel // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable

View File

@@ -86,6 +86,8 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
),
experience: (json['experience'] as num).toInt(),
level: (json['level'] as num).toInt(),
socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100,
socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0,
levelingProgress: (json['leveling_progress'] as num).toDouble(),
picture:
json['picture'] == null
@@ -128,6 +130,8 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'active_badge': instance.activeBadge?.toJson(),
'experience': instance.experience,
'level': instance.level,
'social_credits': instance.socialCredits,
'social_credits_level': instance.socialCreditsLevel,
'leveling_progress': instance.levelingProgress,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),

View File

@@ -54,7 +54,7 @@ sealed class SnEventCalendarEntry with _$SnEventCalendarEntry {
const factory SnEventCalendarEntry({
required DateTime date,
required SnCheckInResult? checkInResult,
required List<dynamic> statuses,
required List<SnAccountStatus> statuses,
}) = _SnEventCalendarEntry;
factory SnEventCalendarEntry.fromJson(Map<String, dynamic> json) =>

View File

@@ -861,7 +861,7 @@ as String,
/// @nodoc
mixin _$SnEventCalendarEntry {
DateTime get date; SnCheckInResult? get checkInResult; List<dynamic> get statuses;
DateTime get date; SnCheckInResult? get checkInResult; List<SnAccountStatus> get statuses;
/// Create a copy of SnEventCalendarEntry
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -894,7 +894,7 @@ abstract mixin class $SnEventCalendarEntryCopyWith<$Res> {
factory $SnEventCalendarEntryCopyWith(SnEventCalendarEntry value, $Res Function(SnEventCalendarEntry) _then) = _$SnEventCalendarEntryCopyWithImpl;
@useResult
$Res call({
DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses
DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses
});
@@ -916,7 +916,7 @@ class _$SnEventCalendarEntryCopyWithImpl<$Res>
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
as DateTime,checkInResult: freezed == checkInResult ? _self.checkInResult : checkInResult // ignore: cast_nullable_to_non_nullable
as SnCheckInResult?,statuses: null == statuses ? _self.statuses : statuses // ignore: cast_nullable_to_non_nullable
as List<dynamic>,
as List<SnAccountStatus>,
));
}
/// Create a copy of SnEventCalendarEntry
@@ -1010,7 +1010,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnEventCalendarEntry() when $default != null:
return $default(_that.date,_that.checkInResult,_that.statuses);case _:
@@ -1031,7 +1031,7 @@ return $default(_that.date,_that.checkInResult,_that.statuses);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses) $default,) {final _that = this;
switch (_that) {
case _SnEventCalendarEntry():
return $default(_that.date,_that.checkInResult,_that.statuses);}
@@ -1048,7 +1048,7 @@ return $default(_that.date,_that.checkInResult,_that.statuses);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses)? $default,) {final _that = this;
switch (_that) {
case _SnEventCalendarEntry() when $default != null:
return $default(_that.date,_that.checkInResult,_that.statuses);case _:
@@ -1063,13 +1063,13 @@ return $default(_that.date,_that.checkInResult,_that.statuses);case _:
@JsonSerializable()
class _SnEventCalendarEntry implements SnEventCalendarEntry {
const _SnEventCalendarEntry({required this.date, required this.checkInResult, required final List<dynamic> statuses}): _statuses = statuses;
const _SnEventCalendarEntry({required this.date, required this.checkInResult, required final List<SnAccountStatus> statuses}): _statuses = statuses;
factory _SnEventCalendarEntry.fromJson(Map<String, dynamic> json) => _$SnEventCalendarEntryFromJson(json);
@override final DateTime date;
@override final SnCheckInResult? checkInResult;
final List<dynamic> _statuses;
@override List<dynamic> get statuses {
final List<SnAccountStatus> _statuses;
@override List<SnAccountStatus> get statuses {
if (_statuses is EqualUnmodifiableListView) return _statuses;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_statuses);
@@ -1109,7 +1109,7 @@ abstract mixin class _$SnEventCalendarEntryCopyWith<$Res> implements $SnEventCal
factory _$SnEventCalendarEntryCopyWith(_SnEventCalendarEntry value, $Res Function(_SnEventCalendarEntry) _then) = __$SnEventCalendarEntryCopyWithImpl;
@override @useResult
$Res call({
DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses
DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses
});
@@ -1131,7 +1131,7 @@ class __$SnEventCalendarEntryCopyWithImpl<$Res>
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
as DateTime,checkInResult: freezed == checkInResult ? _self.checkInResult : checkInResult // ignore: cast_nullable_to_non_nullable
as SnCheckInResult?,statuses: null == statuses ? _self._statuses : statuses // ignore: cast_nullable_to_non_nullable
as List<dynamic>,
as List<SnAccountStatus>,
));
}

View File

@@ -87,7 +87,10 @@ _SnEventCalendarEntry _$SnEventCalendarEntryFromJson(
: SnCheckInResult.fromJson(
json['check_in_result'] as Map<String, dynamic>,
),
statuses: json['statuses'] as List<dynamic>,
statuses:
(json['statuses'] as List<dynamic>)
.map((e) => SnAccountStatus.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$SnEventCalendarEntryToJson(
@@ -95,5 +98,5 @@ Map<String, dynamic> _$SnEventCalendarEntryToJson(
) => <String, dynamic>{
'date': instance.date.toIso8601String(),
'check_in_result': instance.checkInResult?.toJson(),
'statuses': instance.statuses,
'statuses': instance.statuses.map((e) => e.toJson()).toList(),
};

View File

@@ -27,6 +27,7 @@ sealed class SnPost with _$SnPost {
@Default(0) int upvotes,
@Default(0) int downvotes,
@Default(0) int repliesCount,
int? pinMode,
String? threadedPostId,
SnPost? threadedPost,
String? repliedPostId,

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPost {
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
@useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -83,7 +83,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore:
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable
as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // ignore: cast_nullable_to_non_nullable
as int?,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable
as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable
as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable
@@ -242,10 +243,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return orElse();
}
@@ -263,10 +264,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
switch (_that) {
case _SnPost():
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -280,10 +281,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return null;
}
@@ -295,7 +296,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
@JsonSerializable()
class _SnPost implements SnPost {
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@@ -322,6 +323,7 @@ class _SnPost implements SnPost {
@override@JsonKey() final int upvotes;
@override@JsonKey() final int downvotes;
@override@JsonKey() final int repliesCount;
@override final int? pinMode;
@override final String? threadedPostId;
@override final SnPost? threadedPost;
@override final String? repliedPostId;
@@ -398,16 +400,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -418,7 +420,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
@override @useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@@ -435,7 +437,7 @@ class __$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_SnPost(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -453,7 +455,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore:
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable
as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // ignore: cast_nullable_to_non_nullable
as int?,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable
as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable
as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable
as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable

View File

@@ -29,6 +29,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
upvotes: (json['upvotes'] as num?)?.toInt() ?? 0,
downvotes: (json['downvotes'] as num?)?.toInt() ?? 0,
repliesCount: (json['replies_count'] as num?)?.toInt() ?? 0,
pinMode: (json['pin_mode'] as num?)?.toInt(),
threadedPostId: json['threaded_post_id'] as String?,
threadedPost:
json['threaded_post'] == null
@@ -109,6 +110,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'upvotes': instance.upvotes,
'downvotes': instance.downvotes,
'replies_count': instance.repliesCount,
'pin_mode': instance.pinMode,
'threaded_post_id': instance.threadedPostId,
'threaded_post': instance.threadedPost?.toJson(),
'replied_post_id': instance.repliedPostId,

View File

@@ -38,6 +38,7 @@ import 'package:island/screens/chat/chat.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.dart';
import 'package:island/screens/chat/search_messages_screen.dart';
import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart';
@@ -555,6 +556,14 @@ final routerProvider = Provider<GoRouter>((ref) {
return ChatDetailScreen(id: id);
},
),
GoRoute(
name: 'searchMessages',
path: '/chat/:id/search',
builder: (context, state) {
final id = state.pathParameters['id']!;
return SearchMessagesScreen(roomId: id);
},
),
],
),

View File

@@ -95,8 +95,24 @@ class LevelingScreen extends HookConsumerWidget {
title: Text('levelingProgress'.tr()),
bottom: TabBar(
tabs: [
Tab(text: 'leveling'.tr()),
Tab(text: 'stellarProgram'.tr()),
Tab(
child: Text(
'leveling'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'stellarProgram'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),

View File

@@ -279,6 +279,24 @@ class AccountProfileScreen extends HookConsumerWidget {
if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName),
],
),
Tooltip(
message: 'creditsStatus'.tr(),
child: Row(
spacing: 6,
children: [
Icon(Symbols.star, size: 17, fill: 1).padding(right: 2),
Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'),
Text('·').bold(),
switch (data.profile.socialCreditsLevel) {
-1 => Text('socialCreditsLevelPoor').tr(),
0 => Text('socialCreditsLevelNormal').tr(),
1 => Text('socialCreditsLevelGood').tr(),
2 => Text('socialCreditsLevelExcellent').tr(),
_ => Text('unknown').tr(),
},
],
),
),
];
}

View File

@@ -72,6 +72,207 @@ class _AppLifecycleObserver extends WidgetsBindingObserver {
}
}
class _PublicRoomPreview extends HookConsumerWidget {
final String id;
final SnChatRoom room;
const _PublicRoomPreview({required this.id, required this.room});
@override
Widget build(BuildContext context, WidgetRef ref) {
final messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final scrollController = useScrollController();
final listController = useMemoized(() => ListController(), []);
var isLoading = false;
// Add scroll listener for pagination
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (isLoading) return;
isLoading = true;
messagesNotifier.loadMore().then((_) => isLoading = false);
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
SuperListView.builder(
listController: listController,
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey;
final messageId = valueKey.value as String;
return messageList.indexWhere((m) => m.id == messageId);
},
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1 ? messageList[index + 1] : null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return MessageItem(
message: message,
isCurrentUser: false, // User is not a member, so not current user
onAction: null, // No actions allowed in preview mode
onJump: (_) {}, // No jump functionality in preview
progress: null,
showAvatar: isLastInGroup,
);
},
);
final compactHeader = isWideScreen(context);
Widget comfortHeaderWidget() => Column(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(15),
],
);
Widget compactHeaderWidget() => Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(19),
],
);
return AppScaffold(
appBar: AppBar(
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 64,
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
actions: [
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
context.pushNamed('chatDetail', pathParameters: {'id': id});
},
),
const Gap(8),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
// Join button at the bottom for public rooms
Container(
padding: const EdgeInsets.all(16),
child: FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/chat/${room.id}/members/me');
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
),
),
],
),
);
}
}
@riverpod
class MessagesNotifier extends _$MessagesNotifier {
late final Dio _apiClient;
@@ -82,6 +283,9 @@ class MessagesNotifier extends _$MessagesNotifier {
final Map<String, LocalChatMessage> _pendingMessages = {};
final Map<String, Map<int, double>> _fileUploadProgress = {};
int? _totalCount;
String? _searchQuery;
bool? _withLinks;
bool? _withAttachments;
late final String _roomId;
int _currentPage = 0;
@@ -96,28 +300,42 @@ class MessagesNotifier extends _$MessagesNotifier {
_database = ref.watch(databaseProvider);
final room = await ref.watch(chatroomProvider(roomId).future);
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
if (room == null || identity == null) {
throw Exception('Room or identity not found');
if (room == null) {
throw Exception('Room not found');
}
_room = room;
_identity = identity;
// Allow building even if identity is null for public rooms
if (identity != null) {
_identity = identity;
}
developer.log(
'MessagesNotifier built for room $roomId',
name: 'MessagesNotifier',
);
ref.listen(appLifecycleStateProvider, (_, next) {
if (next.hasValue && next.value == AppLifecycleState.resumed) {
developer.log(
'App resumed, syncing messages',
name: 'MessagesNotifier',
);
syncMessages();
}
});
// Only setup sync and lifecycle listeners if user is a member
if (identity != null) {
ref.listen(appLifecycleStateProvider, (_, next) {
if (next.hasValue && next.value == AppLifecycleState.resumed) {
developer.log(
'App resumed, syncing messages',
name: 'MessagesNotifier',
);
syncMessages();
}
});
}
return await loadInitial();
loadInitial();
return [];
}
List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) {
messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return messages;
}
Future<List<LocalChatMessage>> _getCachedMessages({
@@ -128,13 +346,32 @@ class MessagesNotifier extends _$MessagesNotifier {
'Getting cached messages from offset $offset, take $take',
name: 'MessagesNotifier',
);
final dbMessages = await _database.getMessagesForRoom(
_roomId,
offset: offset,
limit: take,
);
final dbLocalMessages =
dbMessages.map(_database.companionToMessage).toList();
final List<LocalChatMessage> dbMessages;
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
dbMessages = await _database.searchMessages(_roomId, _searchQuery ?? '');
} else {
final chatMessagesFromDb = await _database.getMessagesForRoom(
_roomId,
offset: offset,
limit: take,
);
dbMessages =
chatMessagesFromDb.map(_database.companionToMessage).toList();
}
List<LocalChatMessage> filteredMessages = dbMessages;
if (_withLinks == true) {
filteredMessages =
filteredMessages.where((msg) => _hasLink(msg)).toList();
}
if (_withAttachments == true) {
filteredMessages =
filteredMessages.where((msg) => _hasAttachment(msg)).toList();
}
final dbLocalMessages = filteredMessages;
if (offset == 0) {
final pendingForRoom =
@@ -143,7 +380,7 @@ class MessagesNotifier extends _$MessagesNotifier {
.toList();
final allMessages = [...pendingForRoom, ...dbLocalMessages];
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
_sortMessages(allMessages); // Use the helper function
final uniqueMessages = <LocalChatMessage>[];
final seenIds = <String>{};
@@ -218,7 +455,7 @@ class MessagesNotifier extends _$MessagesNotifier {
_isSyncing = true;
developer.log('Starting message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = true;
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
try {
final dbMessages = await _database.getMessagesForRoom(
_room.id,
@@ -279,7 +516,9 @@ class MessagesNotifier extends _$MessagesNotifier {
showErrorAlert(err);
} finally {
developer.log('Finished message sync', name: 'MessagesNotifier');
ref.read(isSyncingProvider.notifier).state = false;
Future.microtask(
() => ref.read(isSyncingProvider.notifier).state = false,
);
_isSyncing = false;
}
}
@@ -290,7 +529,9 @@ class MessagesNotifier extends _$MessagesNotifier {
bool synced = false,
}) async {
try {
if (offset == 0 && !synced) {
if (offset == 0 &&
!synced &&
(_searchQuery == null || _searchQuery!.isEmpty)) {
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
return <LocalChatMessage>[];
});
@@ -305,7 +546,11 @@ class MessagesNotifier extends _$MessagesNotifier {
return localMessages;
}
return await _fetchAndCacheMessages(offset: offset, take: take);
if (_searchQuery == null || _searchQuery!.isEmpty) {
return await _fetchAndCacheMessages(offset: offset, take: take);
} else {
return []; // If searching, and no local messages, don't fetch from network
}
} catch (e) {
final localMessages = await _getCachedMessages(
offset: offset,
@@ -319,13 +564,15 @@ class MessagesNotifier extends _$MessagesNotifier {
}
}
Future<List<LocalChatMessage>> loadInitial() async {
Future<void> loadInitial() async {
developer.log('Loading initial messages', name: 'MessagesNotifier');
syncMessages();
if (_searchQuery == null || _searchQuery!.isEmpty) {
syncMessages();
}
final messages = await _getCachedMessages(offset: 0, take: 100);
_currentPage = 0;
_hasMore = messages.length == _pageSize;
return messages;
state = AsyncValue.data(messages);
}
Future<void> loadMore() async {
@@ -344,7 +591,9 @@ class MessagesNotifier extends _$MessagesNotifier {
_hasMore = false;
}
state = AsyncValue.data([...currentMessages, ...newMessages]);
state = AsyncValue.data(
_sortMessages([...currentMessages, ...newMessages]),
);
} catch (err, stackTrace) {
developer.log(
'Error loading more messages',
@@ -455,10 +704,13 @@ class MessagesNotifier extends _$MessagesNotifier {
final currentMessages = state.value ?? [];
if (editingTo != null) {
final newMessages = currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map((m) => m.id == editingTo.id ? updatedMessage : m) // update original message
.toList();
final newMessages =
currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map(
(m) => m.id == editingTo.id ? updatedMessage : m,
) // update original message
.toList();
state = AsyncValue.data(newMessages);
} else {
final newMessages =
@@ -566,7 +818,7 @@ class MessagesNotifier extends _$MessagesNotifier {
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
state = AsyncValue.data(_sortMessages(newMessages));
showErrorAlert(e);
}
}
@@ -626,7 +878,7 @@ class MessagesNotifier extends _$MessagesNotifier {
if (index >= 0) {
final newList = [...currentMessages];
newList[index] = updatedMessage;
state = AsyncValue.data(newList);
state = AsyncValue.data(_sortMessages(newList));
}
}
@@ -686,6 +938,20 @@ class MessagesNotifier extends _$MessagesNotifier {
}
}
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
_searchQuery = query.trim();
_withLinks = withLinks;
_withAttachments = withAttachments;
loadInitial();
}
void clearSearch() {
_searchQuery = null;
_withLinks = null;
_withAttachments = null;
loadInitial();
}
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
developer.log(
'Fetching message by id $messageId',
@@ -715,6 +981,18 @@ class MessagesNotifier extends _$MessagesNotifier {
rethrow;
}
}
bool _hasLink(LocalChatMessage message) {
final content = message.toRemoteMessage().content;
if (content == null) return false;
final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*');
return urlRegex.hasMatch(content);
}
bool _hasAttachment(LocalChatMessage message) {
final remoteMessage = message.toRemoteMessage();
return remoteMessage.attachments.isNotEmpty;
}
}
class ChatRoomScreen extends HookConsumerWidget {
@@ -734,57 +1012,77 @@ class ChatRoomScreen extends HookConsumerWidget {
);
} else if (chatIdentity.value == null) {
// Identity was not found, user was not joined
return AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(
child:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
chatRoom.value?.isCommunity == true
? Symbols.person_add
: Symbols.person_remove,
size: 36,
fill: 1,
).padding(bottom: 4),
Text('chatNotJoined').tr(),
if (chatRoom.value?.isCommunity != true)
Text(
'chatUnableJoin',
textAlign: TextAlign.center,
).tr().bold()
else
FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
if (chatRoom.value == null) {
hideLoadingModal(context);
return;
}
await apiClient.post(
'/sphere/chat/${chatRoom.value!.id}/members/me',
);
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
).padding(top: 8),
],
),
).center(),
),
return chatRoom.when(
data: (room) {
if (room!.isPublic) {
// Show public room preview with messages but no input
return _PublicRoomPreview(id: id, room: room);
} else {
// Show regular "not joined" screen for private rooms
return AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(
child:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
room.isCommunity == true
? Symbols.person_add
: Symbols.person_remove,
size: 36,
fill: 1,
).padding(bottom: 4),
Text('chatNotJoined').tr(),
if (room.isCommunity != true)
Text(
'chatUnableJoin',
textAlign: TextAlign.center,
).tr().bold()
else
FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post(
'/sphere/chat/${room.id}/members/me',
);
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
).padding(top: 8),
],
),
).center(),
),
);
}
},
loading:
() => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: CircularProgressIndicator().center(),
),
error:
(error, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: ResponseErrorWidget(
error: error,
onRetry: () => ref.refresh(chatroomProvider(id)),
),
),
);
}
@@ -1549,7 +1847,7 @@ class _ChatInput extends HookConsumerWidget {
children: [
IconButton(
tooltip: 'stickers'.tr(),
icon: const Icon(Symbols.emoji_symbols),
icon: const Icon(Symbols.add_reaction),
onPressed: () {
final size = MediaQuery.of(context).size;
showStickerPickerPopover(
@@ -1659,8 +1957,13 @@ class _ChatInput extends HookConsumerWidget {
horizontal: 12,
vertical: 4,
),
counterText:
messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
),
maxLines: null,
maxLines: 3,
minLines: 1,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),

View File

@@ -6,7 +6,7 @@ part of 'room.dart';
// RiverpodGenerator
// **************************************************************************
String _$messagesNotifierHash() => r'32afe6ea24086d869cc47bd3389c8fd734409ca0';
String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart';
@@ -19,10 +20,17 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/database.dart';
part 'room_detail.freezed.dart';
part 'room_detail.g.dart';
@riverpod
Future<int> totalMessagesCount(Ref ref, String roomId) async {
final database = ref.watch(databaseProvider);
return database.getTotalMessagesForRoom(roomId);
}
class ChatDetailScreen extends HookConsumerWidget {
final String id;
const ChatDetailScreen({super.key, required this.id});
@@ -31,6 +39,7 @@ class ChatDetailScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final roomState = ref.watch(chatroomProvider(id));
final roomIdentity = ref.watch(chatroomIdentityProvider(id));
final totalMessages = ref.watch(totalMessagesCountProvider(id));
const kNotifyLevelText = [
'chatNotifyLevelAll',
@@ -131,7 +140,7 @@ class ChatDetailScreen extends HookConsumerWidget {
const Text('chatBreakDescription').tr(),
const Gap(16),
ListTile(
title: const Text('Clear').tr(),
title: const Text('chatBreakClearButton').tr(),
subtitle: const Text('chatBreakClear').tr(),
leading: const Icon(Icons.notifications_active),
onTap: () {
@@ -143,8 +152,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('5m'),
subtitle: const Text('chatBreakHour').tr(args: ['5m']),
title: const Text('chatBreak5m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak5m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 5)));
@@ -155,8 +164,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('10m'),
subtitle: const Text('chatBreakHour').tr(args: ['10m']),
title: const Text('chatBreak10m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak10m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 10)));
@@ -167,8 +176,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('15m'),
subtitle: const Text('chatBreakHour').tr(args: ['15m']),
title: const Text('chatBreak15m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak15m'.tr()]),
leading: const Icon(Symbols.timer_3),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 15)));
@@ -179,8 +188,8 @@ class ChatDetailScreen extends HookConsumerWidget {
},
),
ListTile(
title: const Text('30m'),
subtitle: const Text('chatBreakHour').tr(args: ['30m']),
title: const Text('chatBreak30m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak30m'.tr()]),
leading: const Icon(Symbols.timer),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 30)));
@@ -194,8 +203,8 @@ class ChatDetailScreen extends HookConsumerWidget {
TextField(
controller: durationController,
decoration: InputDecoration(
labelText: 'Custom (minutes)'.tr(),
hintText: 'Enter minutes'.tr(),
labelText: 'chatBreakCustomMinutes'.tr(),
hintText: 'chatBreakEnterMinutes'.tr(),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.check),
@@ -238,7 +247,7 @@ class ChatDetailScreen extends HookConsumerWidget {
return AppScaffold(
body: roomState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
data:
(currentRoom) => CustomScrollView(
slivers: [
@@ -358,6 +367,22 @@ class ChatDetailScreen extends HookConsumerWidget {
: const Text('chatBreakNone').tr(),
onTap: () => showChatBreakDialog(),
),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.search),
trailing: const Icon(Symbols.chevron_right),
title: const Text('searchMessages').tr(),
subtitle: totalMessages.when(
data: (count) => Text('messagesCount'.tr(args: [count.toString()])),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('errorGeneric'.tr(args: [err.toString()])),
),
onTap: () {
context.pushNamed('searchMessages', pathParameters: {'id': id});
},
),
],
),
error: (_, _) => const SizedBox.shrink(),
@@ -666,8 +691,11 @@ class _ChatMemberListSheet extends HookConsumerWidget {
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account.profile.picture?.id,
leading: AccountPfcGestureDetector(
uname: member.account.name,
child: ProfilePictureWidget(
fileId: member.account.profile.picture?.id,
),
),
title: Row(
spacing: 6,
@@ -688,7 +716,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('·').bold().padding(horizontal: 6),
Text('dotSeparator').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account.name}")),
],
),
@@ -848,7 +876,7 @@ class _ChatMemberRoleSheet extends HookConsumerWidget {
try {
final newRole = int.parse(roleController.text);
if (newRole < 0 || newRole > 100) {
throw 'Role must be between 0 and 100';
throw 'roleValidationHint'.tr();
}
final apiClient = ref.read(apiClientProvider);

View File

@@ -6,8 +6,8 @@ part of 'room_detail.dart';
// RiverpodGenerator
// **************************************************************************
String _$chatMemberListNotifierHash() =>
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
String _$totalMessagesCountHash() =>
r'a15c03461f25c2d4d39c0926509bf626ae2550a6';
/// Copied from Dart SDK
class _SystemHash {
@@ -30,6 +30,128 @@ class _SystemHash {
}
}
/// See also [totalMessagesCount].
@ProviderFor(totalMessagesCount)
const totalMessagesCountProvider = TotalMessagesCountFamily();
/// See also [totalMessagesCount].
class TotalMessagesCountFamily extends Family<AsyncValue<int>> {
/// See also [totalMessagesCount].
const TotalMessagesCountFamily();
/// See also [totalMessagesCount].
TotalMessagesCountProvider call(String roomId) {
return TotalMessagesCountProvider(roomId);
}
@override
TotalMessagesCountProvider getProviderOverride(
covariant TotalMessagesCountProvider provider,
) {
return call(provider.roomId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'totalMessagesCountProvider';
}
/// See also [totalMessagesCount].
class TotalMessagesCountProvider extends AutoDisposeFutureProvider<int> {
/// See also [totalMessagesCount].
TotalMessagesCountProvider(String roomId)
: this._internal(
(ref) => totalMessagesCount(ref as TotalMessagesCountRef, roomId),
from: totalMessagesCountProvider,
name: r'totalMessagesCountProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$totalMessagesCountHash,
dependencies: TotalMessagesCountFamily._dependencies,
allTransitiveDependencies:
TotalMessagesCountFamily._allTransitiveDependencies,
roomId: roomId,
);
TotalMessagesCountProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.roomId,
}) : super.internal();
final String roomId;
@override
Override overrideWith(
FutureOr<int> Function(TotalMessagesCountRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TotalMessagesCountProvider._internal(
(ref) => create(ref as TotalMessagesCountRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
roomId: roomId,
),
);
}
@override
AutoDisposeFutureProviderElement<int> createElement() {
return _TotalMessagesCountProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TotalMessagesCountProvider && other.roomId == roomId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, roomId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin TotalMessagesCountRef on AutoDisposeFutureProviderRef<int> {
/// The parameter `roomId` of this provider.
String get roomId;
}
class _TotalMessagesCountProviderElement
extends AutoDisposeFutureProviderElement<int>
with TotalMessagesCountRef {
_TotalMessagesCountProviderElement(super.provider);
@override
String get roomId => (origin as TotalMessagesCountProvider).roomId;
}
String _$chatMemberListNotifierHash() =>
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
abstract class _$ChatMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> {
late final String roomId;

View File

@@ -0,0 +1,139 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/message_item.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class SearchMessagesScreen extends HookConsumerWidget {
final String roomId;
const SearchMessagesScreen({super.key, required this.roomId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchController = useTextEditingController();
final withLinks = useState(false);
final withAttachments = useState(false);
final messagesNotifier = ref.read(
messagesNotifierProvider(roomId).notifier,
);
final messages = ref.watch(messagesNotifierProvider(roomId));
useEffect(() {
// Clear search when screen is disposed
return () {
messagesNotifier.clearSearch();
};
}, []);
return AppScaffold(
appBar: AppBar(title: const Text('searchMessages').tr()),
body: Column(
children: [
Column(
children: [
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'searchMessagesHint'.tr(),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 16,
),
suffix: IconButton(
iconSize: 18,
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
messagesNotifier.clearSearch();
},
),
),
onChanged: (query) {
messagesNotifier.searchMessages(
query,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
Row(
children: [
Expanded(
child: CheckboxListTile(
secondary: const Icon(Symbols.link),
title: const Text('searchLinks').tr(),
value: withLinks.value,
onChanged: (bool? value) {
withLinks.value = value!;
messagesNotifier.searchMessages(
searchController.text,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
),
Expanded(
child: CheckboxListTile(
secondary: const Icon(Symbols.file_copy),
title: const Text('searchAttachments').tr(),
value: withAttachments.value,
onChanged: (bool? value) {
withAttachments.value = value!;
messagesNotifier.searchMessages(
searchController.text,
withLinks: withLinks.value,
withAttachments: withAttachments.value,
);
},
),
),
],
),
],
),
const Divider(height: 1),
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('noMessagesFound'.tr()))
: SuperListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
itemBuilder: (context, index) {
final message = messageList[index];
// Simplified MessageItem for search results, no grouping logic
return MessageItem(
message: message,
isCurrentUser:
false, // Or determine based on actual user
onAction: null,
onJump: (_) {},
progress: null,
showAvatar: true,
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
),
),
],
),
);
}
}

View File

@@ -27,7 +27,9 @@ class AppDetailScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController(initialLength: 2);
final appData = ref.watch(customAppProvider(publisherName, projectId, appId));
final appData = ref.watch(
customAppProvider(publisherName, projectId, appId),
);
return AppScaffold(
appBar: AppBar(
@@ -35,23 +37,43 @@ class AppDetailScreen extends HookConsumerWidget {
actions: [
IconButton(
icon: const Icon(Symbols.edit),
onPressed: appData.value == null
? null
: () {
context.pushNamed(
'developerAppEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': appId,
},
);
},
onPressed:
appData.value == null
? null
: () {
context.pushNamed(
'developerAppEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': appId,
},
);
},
),
],
bottom: TabBar(
controller: tabController,
tabs: [Tab(text: 'overview'.tr()), Tab(text: 'secrets'.tr())],
tabs: [
Tab(
child: Text(
'overview'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'secrets'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
body: appData.when(
@@ -70,12 +92,14 @@ class AppDetailScreen extends HookConsumerWidget {
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(
customAppProvider(publisherName, projectId, appId),
),
),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
customAppProvider(publisherName, projectId, appId),
),
),
),
);
}
@@ -98,12 +122,13 @@ class _AppOverview extends StatelessWidget {
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: app.background != null
? CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
child:
app.background != null
? CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
Positioned(
left: 20,

View File

@@ -52,7 +52,26 @@ class BotDetailScreen extends HookConsumerWidget {
],
bottom: TabBar(
controller: tabController,
tabs: [Tab(text: 'overview'.tr()), Tab(text: 'keys'.tr())],
tabs: [
Tab(
child: Text(
'overview'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'keys'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
body: botData.when(

View File

@@ -58,7 +58,26 @@ class ProjectDetailScreen extends HookConsumerWidget {
],
bottom: TabBar(
controller: tabController,
tabs: [Tab(text: 'customApps'.tr()), Tab(text: 'bots'.tr())],
tabs: [
Tab(
child: Text(
'customApps'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'bots'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
body: TabBarView(

View File

@@ -143,8 +143,26 @@ class ArticlesScreen extends ConsumerWidget {
bottom: TabBar(
isScrollable: true,
tabs: [
const Tab(text: 'All'),
...feeds.map((feed) => Tab(text: feed.title)),
Tab(
child: Text(
'All',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
...feeds.map(
(feed) => Tab(
child: Text(
feed.title,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
),
],
),
),

View File

@@ -288,7 +288,11 @@ class PublisherProfileScreen extends HookConsumerWidget {
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [Tab(text: 'All'), Tab(text: 'Posts'), Tab(text: 'Articles')],
tabs: [
Tab(text: 'all'.tr()),
Tab(text: 'postTypePost'.tr()),
Tab(text: 'postArticle'.tr()),
],
),
);
@@ -345,12 +349,14 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: CustomScrollView(
slivers: [
SliverGap(16),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(
child: publisherCategoryTabWidget(),
),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
pinned: false,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,
@@ -433,10 +439,12 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: publisherVerificationWidget(data),
),
SliverToBoxAdapter(child: publisherBioWidget(data)),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(child: publisherCategoryTabWidget()),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
pinned: false,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:island/models/chat.dart';
import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:palette_generator/palette_generator.dart';
@@ -244,7 +245,10 @@ class RealmDetailScreen extends HookConsumerWidget {
Flexible(
flex: 3,
child: CustomScrollView(
slivers: [SliverPostList(realm: slug)],
slivers: [
SliverPostList(realm: slug, pinned: true),
SliverPostList(realm: slug, pinned: false),
],
),
),
Flexible(
@@ -359,7 +363,8 @@ class RealmDetailScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: realmChatRoomListWidget(realm),
),
SliverPostList(realm: slug),
SliverPostList(realm: slug, pinned: true),
SliverPostList(realm: slug, pinned: false),
],
),
),
@@ -654,8 +659,11 @@ class _RealmMemberListSheet extends HookConsumerWidget {
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
leading: AccountPfcGestureDetector(
uname: member.account!.name,
child: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
),
title: Row(
spacing: 6,

View File

@@ -42,6 +42,16 @@ class AccountName extends StatelessWidget {
StellarMembershipMark(membership: account.perkSubscription!),
if (account.profile.verification != null)
VerificationMark(mark: account.profile.verification!),
if (account.automatedId != null)
Tooltip(
message: 'accountAutomated'.tr(),
child: Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
),
),
],
);
}
@@ -141,7 +151,7 @@ class VerificationStatusCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
mark.type == 4

View File

@@ -1,6 +1,7 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
@@ -74,7 +75,42 @@ class AccountProfileCard extends HookConsumerWidget {
uname: data.name,
padding: EdgeInsets.zero,
),
if (data.profile.timeZone.isNotEmpty)
Tooltip(
message: 'creditsStatus'.tr(),
child: Row(
spacing: 6,
children: [
Icon(
Symbols.star,
size: 17,
fill: 1,
).padding(right: 2),
Text(
'${data.profile.socialCredits.toStringAsFixed(2)} pts',
).fontSize(12),
switch (data.profile.socialCreditsLevel) {
-1 => Text('socialCreditsLevelPoor').tr(),
0 => Text('socialCreditsLevelNormal').tr(),
1 => Text('socialCreditsLevelGood').tr(),
2 => Text('socialCreditsLevelExcellent').tr(),
_ => Text('unknown').tr(),
}.fontSize(12),
],
),
),
if (data.automatedId != null)
Row(
spacing: 6,
children: [
Icon(
Symbols.smart_toy,
size: 17,
fill: 1,
).padding(right: 2),
Text('accountAutomated').tr().fontSize(12),
],
),
if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Row(
spacing: 6,
children: [

View File

@@ -2,7 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/models/activity.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/account/event_details_widget.dart';
import 'package:table_calendar/table_calendar.dart';
@@ -87,24 +89,56 @@ class EventCalendarWidget extends HookConsumerWidget {
return Center(child: Text(text));
},
markerBuilder: (context, day, events) {
var checkInResult =
final checkInResult =
events.whereType<SnCheckInResult>().firstOrNull;
final statuses = events.whereType<SnAccountStatus>().toList();
final textColor =
isSameDay(selectedDay.value, day)
? Colors.white
: isSameDay(DateTime.now(), day)
? Colors.white
: Theme.of(context).colorScheme.onSurface;
final shadow =
isSameDay(selectedDay.value, day) ||
isSameDay(DateTime.now(), day)
? [
Shadow(
color: Colors.black.withOpacity(0.5),
offset: const Offset(0, 1),
blurRadius: 4,
),
]
: null;
if (checkInResult != null) {
return Positioned(
top: 32,
child: Text(
'checkInResultT${checkInResult.level}'.tr(),
style: TextStyle(
fontSize: 9,
color:
isSameDay(selectedDay.value, day)
? Theme.of(context).colorScheme.onPrimaryContainer
: isSameDay(DateTime.now(), day)
? Theme.of(
context,
).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
child: Row(
spacing: 2,
children: [
Text(
'checkInResultT${checkInResult.level}'.tr(),
style: TextStyle(
fontSize: 9,
color: textColor,
shadows: shadow,
),
),
if (statuses.isNotEmpty) ...[
Icon(
switch (statuses.first.attitude) {
0 => Symbols.sentiment_satisfied,
2 => Symbols.sentiment_dissatisfied,
_ => Symbols.sentiment_neutral,
},
size: 12,
color: textColor,
shadows: shadow,
),
],
],
),
);
}

View File

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/activity.dart';
import 'package:island/services/time.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -53,6 +54,33 @@ class EventDetailsWidget extends StatelessWidget {
),
],
).padding(top: 8),
if (event!.statuses.isNotEmpty) ...[
const Gap(16),
Text('statusLabel').tr().fontSize(16).bold(),
],
for (final status in event!.statuses) ...[
Row(
spacing: 8,
children: [
Icon(switch (status.attitude) {
0 => Symbols.sentiment_satisfied,
2 => Symbols.sentiment_dissatisfied,
_ => Symbols.sentiment_neutral,
}),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(status.label),
Text(
'${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}',
).fontSize(11).opacity(0.8),
],
),
),
],
).padding(vertical: 8),
],
],
),
if (event?.checkInResult == null && (event?.statuses.isEmpty ?? true))

View File

@@ -1,9 +1,12 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
@@ -321,7 +324,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
Future<void> saveToGallery() async {
try {
// Show loading indicator
showSnackBar('Saving image to gallery...');
showSnackBar('Saving image...');
// Get the image URL
final client = ref.watch(apiClientProvider);
@@ -339,10 +342,18 @@ class CloudFileZoomIn extends HookConsumerWidget {
filePath,
queryParameters: {'original': true},
);
await Gal.putImage(filePath, album: 'Solar Network');
// Show success message
showSnackBar('Image saved to gallery');
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
// Save to gallery
await Gal.putImage(filePath, album: 'Solar Network');
// Show success message
showSnackBar('Image saved to gallery');
} else {
await FileSaver.instance.saveFile(
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
file: File(filePath),
);
showSnackBar('Image saved to $filePath');
}
} catch (e) {
showErrorAlert(e);
}
@@ -437,7 +448,24 @@ class CloudFileZoomIn extends HookConsumerWidget {
).padding(horizontal: 24, vertical: 16),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.file_present),
leading: const Icon(Symbols.tag),
title: Text('ID').tr(),
subtitle: Text(
item.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: item.id));
showSnackBar('File ID copied to clipboard');
},
),
),
ListTile(
leading: const Icon(Symbols.file_present),
title: Text('Name').tr(),
subtitle: Text(
item.name,
@@ -623,6 +651,10 @@ class CloudFileZoomIn extends HookConsumerWidget {
);
}
final shadow = [
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
];
return DismissiblePage(
isFullScreen: true,
backgroundColor: Colors.transparent,
@@ -660,22 +692,17 @@ class CloudFileZoomIn extends HookConsumerWidget {
children: [
Row(
children: [
IconButton(
icon: Icon(
Icons.save_alt,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
if (!kIsWeb)
IconButton(
icon: Icon(
Icons.save_alt,
color: Colors.white,
shadows: shadow,
),
onPressed: () async {
saveToGallery();
},
),
onPressed: () async {
saveToGallery();
},
),
IconButton(
onPressed: () {
showOriginal.value = !showOriginal.value;
@@ -683,29 +710,13 @@ class CloudFileZoomIn extends HookConsumerWidget {
icon: Icon(
showOriginal.value ? Symbols.hd : Symbols.sd,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
shadows: shadow,
),
),
],
),
IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
icon: Icon(Icons.close, color: Colors.white, shadows: shadow),
onPressed: () => Navigator.of(context).pop(),
),
],
@@ -722,26 +733,24 @@ class CloudFileZoomIn extends HookConsumerWidget {
icon: Icon(
Icons.info_outline,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
shadows: shadow,
),
onPressed: showInfoSheet,
),
Spacer(),
IconButton(
icon: Icon(Icons.remove, color: Colors.white),
icon: Icon(
Icons.remove,
color: Colors.white,
shadows: shadow,
),
onPressed: () {
photoViewController.scale =
(photoViewController.scale ?? 1) - 0.05;
},
),
IconButton(
icon: Icon(Icons.add, color: Colors.white),
icon: Icon(Icons.add, color: Colors.white, shadows: shadow),
onPressed: () {
photoViewController.scale =
(photoViewController.scale ?? 1) + 0.05;
@@ -752,13 +761,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
icon: Icon(
Icons.rotate_left,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
shadows: shadow,
),
onPressed: () {
rotation.value = (rotation.value - 1) % 4;

View File

@@ -18,6 +18,7 @@ import 'package:island/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/post/post_item_screenshot.dart';
import 'package:island/widgets/post/post_pin_sheet.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/widgets/share/share_sheet.dart';
@@ -202,6 +203,45 @@ class PostActionableItem extends HookConsumerWidget {
);
},
),
if (isAuthor && item.pinMode == null)
MenuAction(
title: 'pinPost'.tr(),
image: MenuImage.icon(Symbols.keep),
callback: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => PostPinSheet(post: item),
).then((value) {
if (value is int) {
onUpdate?.call(item.copyWith(pinMode: value));
}
});
},
)
else if (isAuthor && item.pinMode != null)
MenuAction(
title: 'unpinPost'.tr(),
image: MenuImage.icon(Symbols.keep_off),
callback: () {
showConfirmAlert('unpinPostHint'.tr(), 'unpinPost'.tr()).then(
(confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
try {
if (context.mounted) showLoadingModal(context);
await client.delete('/sphere/posts/${item.id}/pin');
onUpdate?.call(item.copyWith(pinMode: null));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
},
);
},
),
MenuSeparator(),
MenuAction(
title: 'share'.tr(),

View File

@@ -21,6 +21,7 @@ class PostListNotifier extends _$PostListNotifier
int? type,
List<String>? categories,
List<String>? tags,
bool? pinned,
bool shuffle = false,
}) {
return fetch(cursor: null);
@@ -40,6 +41,7 @@ class PostListNotifier extends _$PostListNotifier
if (tags != null) 'tags': tags,
if (categories != null) 'categories': categories,
if (shuffle) 'shuffle': true,
if (pinned != null) 'pinned': pinned,
};
final response = await client.get(
@@ -77,6 +79,7 @@ class SliverPostList extends HookConsumerWidget {
final List<String>? categories;
final List<String>? tags;
final bool shuffle;
final bool? pinned;
final PostItemType itemType;
final Color? backgroundColor;
final EdgeInsets? padding;
@@ -93,6 +96,7 @@ class SliverPostList extends HookConsumerWidget {
this.categories,
this.tags,
this.shuffle = false,
this.pinned,
this.itemType = PostItemType.regular,
this.backgroundColor,
this.padding,
@@ -104,33 +108,19 @@ class SliverPostList extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider = postListNotifierProvider(
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
pinned: pinned,
);
return PagingHelperSliverView(
provider: postListNotifierProvider(
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
),
futureRefreshable:
postListNotifierProvider(
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
).future,
notifierRefreshable:
postListNotifierProvider(
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
).notifier,
provider: provider,
futureRefreshable: provider.future,
notifierRefreshable: provider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$postListNotifierHash() => r'faa0b939fae56367ff120ce63d9deb17b1995c9c';
String _$postListNotifierHash() => r'3c0a8154ded4bcd8f5456f7a4ea2e542f57efa85';
/// Copied from Dart SDK
class _SystemHash {
@@ -36,6 +36,7 @@ abstract class _$PostListNotifier
late final int? type;
late final List<String>? categories;
late final List<String>? tags;
late final bool? pinned;
late final bool shuffle;
FutureOr<CursorPagingData<SnPost>> build({
@@ -44,6 +45,7 @@ abstract class _$PostListNotifier
int? type,
List<String>? categories,
List<String>? tags,
bool? pinned,
bool shuffle = false,
});
}
@@ -65,6 +67,7 @@ class PostListNotifierFamily
int? type,
List<String>? categories,
List<String>? tags,
bool? pinned,
bool shuffle = false,
}) {
return PostListNotifierProvider(
@@ -73,6 +76,7 @@ class PostListNotifierFamily
type: type,
categories: categories,
tags: tags,
pinned: pinned,
shuffle: shuffle,
);
}
@@ -87,6 +91,7 @@ class PostListNotifierFamily
type: provider.type,
categories: provider.categories,
tags: provider.tags,
pinned: provider.pinned,
shuffle: provider.shuffle,
);
}
@@ -120,6 +125,7 @@ class PostListNotifierProvider
int? type,
List<String>? categories,
List<String>? tags,
bool? pinned,
bool shuffle = false,
}) : this._internal(
() =>
@@ -129,6 +135,7 @@ class PostListNotifierProvider
..type = type
..categories = categories
..tags = tags
..pinned = pinned
..shuffle = shuffle,
from: postListNotifierProvider,
name: r'postListNotifierProvider',
@@ -144,6 +151,7 @@ class PostListNotifierProvider
type: type,
categories: categories,
tags: tags,
pinned: pinned,
shuffle: shuffle,
);
@@ -159,6 +167,7 @@ class PostListNotifierProvider
required this.type,
required this.categories,
required this.tags,
required this.pinned,
required this.shuffle,
}) : super.internal();
@@ -167,6 +176,7 @@ class PostListNotifierProvider
final int? type;
final List<String>? categories;
final List<String>? tags;
final bool? pinned;
final bool shuffle;
@override
@@ -179,6 +189,7 @@ class PostListNotifierProvider
type: type,
categories: categories,
tags: tags,
pinned: pinned,
shuffle: shuffle,
);
}
@@ -195,6 +206,7 @@ class PostListNotifierProvider
..type = type
..categories = categories
..tags = tags
..pinned = pinned
..shuffle = shuffle,
from: from,
name: null,
@@ -206,6 +218,7 @@ class PostListNotifierProvider
type: type,
categories: categories,
tags: tags,
pinned: pinned,
shuffle: shuffle,
),
);
@@ -228,6 +241,7 @@ class PostListNotifierProvider
other.type == type &&
other.categories == categories &&
other.tags == tags &&
other.pinned == pinned &&
other.shuffle == shuffle;
}
@@ -239,6 +253,7 @@ class PostListNotifierProvider
hash = _SystemHash.combine(hash, type.hashCode);
hash = _SystemHash.combine(hash, categories.hashCode);
hash = _SystemHash.combine(hash, tags.hashCode);
hash = _SystemHash.combine(hash, pinned.hashCode);
hash = _SystemHash.combine(hash, shuffle.hashCode);
return _SystemHash.finish(hash);
@@ -264,6 +279,9 @@ mixin PostListNotifierRef
/// The parameter `tags` of this provider.
List<String>? get tags;
/// The parameter `pinned` of this provider.
bool? get pinned;
/// The parameter `shuffle` of this provider.
bool get shuffle;
}
@@ -289,6 +307,8 @@ class _PostListNotifierProviderElement
@override
List<String>? get tags => (origin as PostListNotifierProvider).tags;
@override
bool? get pinned => (origin as PostListNotifierProvider).pinned;
@override
bool get shuffle => (origin as PostListNotifierProvider).shuffle;
}

View File

@@ -0,0 +1,124 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class PostPinSheet extends HookConsumerWidget {
final SnPost post;
const PostPinSheet({super.key, required this.post});
@override
Widget build(BuildContext context, WidgetRef ref) {
final mode = useState(0);
Future<void> pinPost() async {
try {
showLoadingModal(context);
final client = ref.watch(apiClientProvider);
await client.post(
'/sphere/posts/${post.id}/pin',
data: {'mode': mode.value},
);
if (context.mounted) Navigator.of(context).pop(mode.value);
} catch (e) {
showErrorAlert(e);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'pinPost'.tr(),
heightFactor: 0.6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Publisher page pin option (always available)
ListTile(
leading: Radio<int>(
value: 0,
groupValue: mode.value,
onChanged: (value) {
mode.value = value!;
},
),
title: Text('publisherPage'.tr()),
subtitle: Text('pinPostPublisherHint'.tr()),
onTap: () {
mode.value = 0;
},
),
// Realm page pin option (show always, but disabled when not available)
ListTile(
leading: Radio<int>(
value: 1,
groupValue: mode.value,
onChanged:
post.realmId != null && post.realmId!.isNotEmpty
? (value) {
mode.value = value!;
}
: null,
),
title: Text('realmPage'.tr()),
subtitle:
post.realmId != null && post.realmId!.isNotEmpty
? Text('pinPostRealmHint'.tr())
: Text('pinPostRealmDisabledHint'.tr()),
onTap:
post.realmId != null && post.realmId!.isNotEmpty
? () {
mode.value = 1;
}
: null,
enabled: post.realmId != null && post.realmId!.isNotEmpty,
),
// Reply page pin option (show always, but disabled when not available)
// Disabled for now because im being lazy
// ListTile(
// leading: Radio<int>(
// value: 2,
// groupValue: mode.value,
// onChanged:
// post.repliedPostId != null && post.repliedPostId!.isNotEmpty
// ? (value) {
// mode.value = value!;
// }
// : null,
// ),
// title: Text('replyPage'.tr()),
// subtitle:
// post.repliedPostId != null && post.repliedPostId!.isNotEmpty
// ? Text('pinPostReplyHint'.tr())
// : Text('pinPostReplyDisabledHint'.tr()),
// onTap:
// post.repliedPostId != null && post.repliedPostId!.isNotEmpty
// ? () {
// mode.value = 2;
// }
// : null,
// enabled:
// post.repliedPostId != null && post.repliedPostId!.isNotEmpty,
// ),
const SizedBox(height: 16),
// Pin button
FilledButton.icon(
onPressed: pinPost,
icon: const Icon(Symbols.keep),
label: Text('pin'.tr()),
).padding(horizontal: 24),
],
),
);
}
}

View File

@@ -545,107 +545,119 @@ class PostHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 12,
return Column(
children: [
GestureDetector(
onTap:
isInteractive
? () {
context.pushNamed(
'publisherProfile',
pathParameters: {'name': item.publisher.name},
);
}
: null,
child: ProfilePictureWidget(
file: item.publisher.picture,
radius: 16,
borderRadius: item.publisher.type == 0 ? null : 6,
),
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
if (item.pinMode != null)
Row(
spacing: 4,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 4,
const Icon(Symbols.keep, size: 15, fill: 1),
Text('pinnedPost').tr().fontSize(13),
],
).opacity(0.8).padding(horizontal: 8, bottom: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 12,
children: [
GestureDetector(
onTap:
isInteractive
? () {
context.pushNamed(
'publisherProfile',
pathParameters: {'name': item.publisher.name},
);
}
: null,
child: ProfilePictureWidget(
file: item.publisher.picture,
radius: 16,
borderRadius: item.publisher.type == 0 ? null : 6,
),
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.publisher.nick).bold(),
if (item.publisher.verification != null)
VerificationMark(mark: item.publisher.verification!),
if (item.realm == null)
Text('@${item.publisher.name}').fontSize(11)
else
...([
const Icon(Symbols.arrow_right, size: 14),
Flexible(
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 5,
children: [
Flexible(
child: Text(
item.realm!.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ProfilePictureWidget(
file: item.realm!.picture,
fallbackIcon: Symbols.group,
radius: 9,
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 4,
children: [
Text(item.publisher.nick).bold(),
if (item.publisher.verification != null)
VerificationMark(mark: item.publisher.verification!),
if (item.realm == null)
Text('@${item.publisher.name}').fontSize(11)
else
...([
const Icon(Symbols.arrow_right, size: 14),
Flexible(
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 5,
children: [
Flexible(
child: Text(
item.realm!.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ProfilePictureWidget(
file: item.realm!.picture,
fallbackIcon: Symbols.group,
radius: 9,
),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'slug': item.realm!.slug},
);
},
),
),
]),
],
),
Row(
spacing: 6,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
!isFullPost && isRelativeTime
? (item.publishedAt ?? item.createdAt)!
.formatRelative(context)
: (item.publishedAt ?? item.createdAt)!
.formatSystem(),
).fontSize(10),
if (item.editedAt != null)
Text(
'editedAt'.tr(
args: [
!isFullPost && isRelativeTime
? item.editedAt!.formatRelative(context)
: item.editedAt!.formatSystem(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'slug': item.realm!.slug},
);
},
),
),
]),
).fontSize(10),
if (item.visibility != 0)
Text(
PostVisibilityHelpers.getVisibilityText(
item.visibility,
).tr(),
).fontSize(10),
],
),
],
),
Row(
spacing: 6,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
!isFullPost && isRelativeTime
? (item.publishedAt ?? item.createdAt)!.formatRelative(
context,
)
: (item.publishedAt ?? item.createdAt)!.formatSystem(),
).fontSize(10),
if (item.editedAt != null)
Text(
'editedAt'.tr(
args: [
!isFullPost && isRelativeTime
? item.editedAt!.formatRelative(context)
: item.editedAt!.formatSystem(),
],
),
).fontSize(10),
if (item.visibility != 0)
Text(
PostVisibilityHelpers.getVisibilityText(
item.visibility,
).tr(),
).fontSize(10),
],
),
],
),
),
if (trailing != null) trailing!,
],
),
if (trailing != null) trailing!,
],
).padding(horizontal: renderingPadding.horizontal, bottom: 4);
}

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
@@ -28,6 +29,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
g_autoptr(FlPluginRegistrar) file_saver_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
file_saver_plugin_register_with_registrar(file_saver_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_linux
file_saver
file_selector_linux
flutter_platform_alert
flutter_secure_storage_linux

View File

@@ -9,6 +9,7 @@ import bitsdojo_window_macos
import connectivity_plus
import device_info_plus
import file_picker
import file_saver
import file_selector_macos
import firebase_analytics
import firebase_core
@@ -45,6 +46,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))

View File

@@ -9,6 +9,8 @@ PODS:
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- file_saver (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/CoreOnly (12.0.0):
@@ -249,6 +251,7 @@ DEPENDENCIES:
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
@@ -315,6 +318,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_saver:
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
firebase_analytics:
@@ -384,6 +389,7 @@ SPEC CHECKSUMS:
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: 53f0dc87ad10f56a6df8746da60d8a5fe41f886f

View File

@@ -569,6 +569,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "10.3.2"
file_saver:
dependency: "direct main"
description:
name: file_saver
sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
file_selector_linux:
dependency: transitive
description:
@@ -1921,10 +1929,10 @@ packages:
dependency: transitive
description:
name: record_ios
sha256: "895c9467faec72d8e718a3142b51114958f42f18053836a8b484a74f9372f51a"
sha256: "13e241ed9cbc220534a40ae6b66222e21288db364d96dd66fb762ebd3cb77c71"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
record_linux:
dependency: transitive
description:
@@ -2041,10 +2049,10 @@ packages:
dependency: transitive
description:
name: screen_brightness_android
sha256: fb5fa43cb89d0c9b8534556c427db1e97e46594ac5d66ebdcf16063b773d54ed
sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
screen_brightness_platform_interface:
dependency: transitive
description:
@@ -2640,10 +2648,10 @@ packages:
dependency: transitive
description:
name: watcher
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.3"
waveform_flutter:
dependency: "direct main"
description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 3.2.0+128
version: 3.2.0+129
environment:
sdk: ^3.7.2
@@ -139,6 +139,7 @@ dependencies:
material_color_utilities: ^0.11.1
screenshot: ^3.0.0
flutter_card_swiper: ^7.0.2
file_saver: ^0.3.1
dev_dependencies:
flutter_test:
@@ -235,4 +236,5 @@ msix_config:
identity_name: dev.solian.app
msix_version: 3.2.0.0
logo_path: .\assets\icons\icon.png
capabilities: internetClientServer, location, microphone, webcam
capabilities: internetClientServer, location, microphone, webcam

View File

@@ -8,6 +8,7 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
@@ -35,6 +36,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSaverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSaverPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseCorePluginCApiRegisterWithRegistrar(

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows
connectivity_plus
file_saver
file_selector_windows
firebase_core
flutter_inappwebview_windows