Compare commits

...

12 Commits

Author SHA1 Message Date
446c33d8b0 🐛 Fix wrong route path 2025-07-06 12:28:42 +08:00
996462f1fd 🚀 Launch 3.0.0+112 2025-07-03 22:25:02 +08:00
778f6bb79f Android deeplinking 2025-07-03 22:18:02 +08:00
8747f948b9 Apple deeplinking 2025-07-03 22:14:34 +08:00
9546d6e4b8 🗑️ Disable the technical review tour 2025-07-03 22:11:12 +08:00
f8d1940af6 Report user 2025-07-03 22:07:51 +08:00
b2b0891d24 Block user 2025-07-03 22:03:12 +08:00
274168d4bc 💄 Better abuse reports 2025-07-03 21:31:37 +08:00
2c98b348d5 Abuse reports 2025-07-03 20:53:30 +08:00
afc7887ddd 🐛 Fix delete post on explore didn't refresh 2025-07-03 19:08:56 +08:00
99ff78a3d5 🐛 Fix post item padding 2025-07-03 13:12:39 +08:00
2ad85addf6 🐛 Fix wrong notification permission is requested 2025-07-03 01:29:43 +08:00
30 changed files with 827 additions and 89 deletions

View File

@ -42,6 +42,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deeplinking -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="solian.app" />
<data android:scheme="https" />
</intent-filter>
<!-- Share Intent Filters -->
<intent-filter>
<action android:name="android.intent.action.SEND" />

View File

@ -590,6 +590,7 @@
"yes": "Yes",
"navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
"abuseReports": "Abuse Reports",
"abuseReport": "Report",
"abuseReportTitle": "Report Content",
"abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",

View File

@ -772,6 +772,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -1202,6 +1203,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -1229,6 +1231,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@ -2,16 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>BUNDLE_ID</key>
<string>dev.solsynth.solian</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@ -32,8 +26,6 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@ -45,18 +37,35 @@
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSFaceIDUsageDescription</key>
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
<key>PLIST_VERSION</key>
<string>1</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
@ -75,24 +84,13 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSFaceIDUsageDescription</key>
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,23 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'abuse_report.freezed.dart';
part 'abuse_report.g.dart';
@freezed
sealed class SnAbuseReport with _$SnAbuseReport {
const factory SnAbuseReport({
required String id,
required String resourceIdentifier,
required int type,
required String reason,
required DateTime? resolvedAt,
required String? resolution,
required String accountId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAbuseReport;
factory SnAbuseReport.fromJson(Map<String, dynamic> json) =>
_$SnAbuseReportFromJson(json);
}

View File

@ -0,0 +1,175 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'abuse_report.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnAbuseReport {
String get id; String get resourceIdentifier; int get type; String get reason; DateTime? get resolvedAt; String? get resolution; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAbuseReportCopyWith<SnAbuseReport> get copyWith => _$SnAbuseReportCopyWithImpl<SnAbuseReport>(this as SnAbuseReport, _$identity);
/// Serializes this SnAbuseReport to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAbuseReportCopyWith<$Res> {
factory $SnAbuseReportCopyWith(SnAbuseReport value, $Res Function(SnAbuseReport) _then) = _$SnAbuseReportCopyWithImpl;
@useResult
$Res call({
String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnAbuseReportCopyWithImpl<$Res>
implements $SnAbuseReportCopyWith<$Res> {
_$SnAbuseReportCopyWithImpl(this._self, this._then);
final SnAbuseReport _self;
final $Res Function(SnAbuseReport) _then;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,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,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnAbuseReport implements SnAbuseReport {
const _SnAbuseReport({required this.id, required this.resourceIdentifier, required this.type, required this.reason, required this.resolvedAt, required this.resolution, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAbuseReport.fromJson(Map<String, dynamic> json) => _$SnAbuseReportFromJson(json);
@override final String id;
@override final String resourceIdentifier;
@override final int type;
@override final String reason;
@override final DateTime? resolvedAt;
@override final String? resolution;
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAbuseReportCopyWith<_SnAbuseReport> get copyWith => __$SnAbuseReportCopyWithImpl<_SnAbuseReport>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAbuseReportToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAbuseReportCopyWith<$Res> implements $SnAbuseReportCopyWith<$Res> {
factory _$SnAbuseReportCopyWith(_SnAbuseReport value, $Res Function(_SnAbuseReport) _then) = __$SnAbuseReportCopyWithImpl;
@override @useResult
$Res call({
String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnAbuseReportCopyWithImpl<$Res>
implements _$SnAbuseReportCopyWith<$Res> {
__$SnAbuseReportCopyWithImpl(this._self, this._then);
final _SnAbuseReport _self;
final $Res Function(_SnAbuseReport) _then;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAbuseReport(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'abuse_report.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
_SnAbuseReport(
id: json['id'] as String,
resourceIdentifier: json['resource_identifier'] as String,
type: (json['type'] as num).toInt(),
reason: json['reason'] as String,
resolvedAt:
json['resolved_at'] == null
? null
: DateTime.parse(json['resolved_at'] as String),
resolution: json['resolution'] as String?,
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
<String, dynamic>{
'id': instance.id,
'resource_identifier': instance.resourceIdentifier,
'type': instance.type,
'reason': instance.reason,
'resolved_at': instance.resolvedAt?.toIso8601String(),
'resolution': instance.resolution,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -0,0 +1,38 @@
enum AbuseReportType {
copyright(0),
harassment(1),
impersonation(2),
offensiveContent(3),
spam(4),
privacyViolation(5),
illegalContent(6),
other(7);
const AbuseReportType(this.value);
final int value;
static AbuseReportType fromValue(int value) {
return values.firstWhere((e) => e.value == value);
}
String get displayName {
switch (this) {
case AbuseReportType.copyright:
return 'Copyright';
case AbuseReportType.harassment:
return 'Harassment';
case AbuseReportType.impersonation:
return 'Impersonation';
case AbuseReportType.offensiveContent:
return 'Offensive Content';
case AbuseReportType.spam:
return 'Spam';
case AbuseReportType.privacyViolation:
return 'Privacy Violation';
case AbuseReportType.illegalContent:
return 'Illegal Content';
case AbuseReportType.other:
return 'Other';
}
}
}

View File

@ -11,7 +11,7 @@ import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart';
import 'package:island/screens/explore.dart';
import 'package:island/screens/article_detail_screen.dart';
import 'package:island/screens/discovery/article_detail.dart';
import 'package:island/screens/account.dart';
import 'package:island/screens/notification.dart';
import 'package:island/screens/wallet.dart';
@ -41,6 +41,8 @@ import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/realm_detail.dart';
import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
import 'package:island/screens/reports/report_detail.dart';
import 'package:island/screens/reports/report_list.dart';
// Shell route keys for nested navigation
final rootNavigatorKey = GlobalKey<NavigatorState>();
@ -258,6 +260,19 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const AboutScreen(),
),
GoRoute(
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
GoRoute(
path: '/safety/reports/me/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return AbuseReportDetailScreen(reportId: id);
},
),
// Main tabs with TabsScreen shell
ShellRoute(
navigatorKey: _tabsShellKey,
@ -399,7 +414,7 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const LevelingScreen(),
),
GoRoute(
path: '/account/settings',
path: '/account/me/settings',
builder: (context, state) => const AccountSettingsScreen(),
),
],

View File

@ -7,6 +7,7 @@ import 'package:island/pods/network.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/udid.native.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart';
@ -90,7 +91,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
return AppScaffold(
appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body:
_isLoading

View File

@ -222,9 +222,17 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('relationships').tr(),
onTap: () {
context.push('/account/relationship');
context.push('/account/relationships');
},
),
ListTile(
minTileHeight: 48,
title: Text('abuseReports').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.push('/safety/reports/me'),
),
const Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
@ -328,7 +336,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Card(
child: InkWell(
onTap: () {
context.push('/auth/create');
context.push('/auth/create-account');
},
child: Padding(
padding: const EdgeInsets.all(16),

View File

@ -22,6 +22,7 @@ import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -143,6 +144,23 @@ class AccountProfileScreen extends HookConsumerWidget {
}
}
Future<void> blockAction() async {
showLoadingModal(context);
try {
final client = ref.watch(apiClientProvider);
if (accountRelationship.value == null) {
await client.post('/relationships/${account.value!.id}/block');
} else {
await client.delete('/relationships/${account.value!.id}/block');
}
ref.invalidate(accountRelationshipProvider(name));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> directMessageAction() async {
if (!account.hasValue) return;
if (accountChat.value != null) {
@ -396,6 +414,8 @@ class AccountProfileScreen extends HookConsumerWidget {
child: Row(
spacing: 8,
children: [
if (accountRelationship.value == null ||
accountRelationship.value!.status > -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
@ -407,7 +427,9 @@ class AccountProfileScreen extends HookConsumerWidget {
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
: Theme.of(
context,
).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
@ -423,6 +445,44 @@ class AccountProfileScreen extends HookConsumerWidget {
: const Icon(Symbols.person_check),
),
),
if (accountRelationship.value == null ||
accountRelationship.value!.status <= -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
),
),
onPressed: blockAction,
label:
Text(
accountRelationship.value == null
? 'blockUser'
: 'unblockUser',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.block)
: const Icon(Symbols.person_cancel),
),
),
],
).padding(horizontal: 16),
),
SliverToBoxAdapter(
child: Row(
spacing: 8,
children: [
Expanded(
child: FilledButton.icon(
onPressed: directMessageAction,
@ -436,8 +496,25 @@ class AccountProfileScreen extends HookConsumerWidget {
).tr(),
),
),
IconButton.filled(
onPressed: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'account/${data.id}',
);
},
icon: Icon(
Symbols.flag,
color: Theme.of(context).colorScheme.onError,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.error,
),
),
),
],
).padding(horizontal: 16),
).padding(horizontal: 16, top: 4),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 12),

View File

@ -395,7 +395,7 @@ class _AccountAppbarForcegroundColorProviderElement
String get uname => (origin as AccountAppbarForcegroundColorProvider).uname;
}
String _$accountDirectChatHash() => r'60d0015fc2a3c8fc2190bb41d6818cf3027d9d0a';
String _$accountDirectChatHash() => r'3d28c8ba8079159f724fe3cd47bbe00db55cedcc';
/// See also [accountDirectChat].
@ProviderFor(accountDirectChat)
@ -517,7 +517,7 @@ class _AccountDirectChatProviderElement
}
String _$accountRelationshipHash() =>
r'cb7d0d3f8cd4f23ad9d2d529872c540dac483d4f';
r'0be2420e1f6a65b8dcead9617191471924aaf232';
/// See also [accountRelationship].
@ProviderFor(accountRelationship)

View File

@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@ -183,7 +184,7 @@ class WebFeedEditScreen extends HookConsumerWidget {
}
}, [pubName, feedId, ref, context]);
return Scaffold(
return AppScaffold(
appBar: AppBar(
title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
actions: [

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/web_article_card.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@ -124,7 +125,7 @@ class ArticlesScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
return AppScaffold(
appBar: AppBar(title: Text(title ?? 'Articles')),
body: Center(
child: ConstrainedBox(

View File

@ -338,7 +338,7 @@ class _ActivityListView extends HookConsumerWidget {
bottom: 16,
)
: null,
onRefresh: (_) {
onRefresh: () {
activitiesNotifier.forceRefresh();
},
onUpdate: (post) {

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@ -107,7 +108,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
return AppScaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,

View File

@ -187,7 +187,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
),
onTap: () {
Navigator.pop(context, true);
context.push('/account/${data.name}');
context.push('/account/${data.account?.name}');
},
),
Expanded(

View File

@ -77,6 +77,7 @@ class RealmDetailScreen extends HookConsumerWidget {
);
return AppScaffold(
noBackground: false,
body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/models/abuse_report_type.dart';
import 'package:island/services/abuse_report_service.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:styled_widget/styled_widget.dart';
class AbuseReportDetailScreen extends ConsumerStatefulWidget {
final String reportId;
const AbuseReportDetailScreen({super.key, required this.reportId});
@override
ConsumerState<AbuseReportDetailScreen> createState() =>
_AbuseReportDetailScreenState();
}
class _AbuseReportDetailScreenState
extends ConsumerState<AbuseReportDetailScreen> {
Future<SnAbuseReport>? _reportFuture;
@override
void initState() {
super.initState();
_reportFuture = ref
.read(abuseReportServiceProvider)
.getReport(widget.reportId);
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('Abuse Report Details')),
body: FutureBuilder<SnAbuseReport>(
future: _reportFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final report = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(context, 'Report ID', report.id),
_buildDetailRow(
context,
'Resource Identifier',
report.resourceIdentifier,
),
_buildDetailRow(
context,
'Type',
AbuseReportType.fromValue(report.type).displayName,
),
_buildDetailRow(context, 'Reason', report.reason),
_buildDetailRow(
context,
'Resolved At',
report.resolvedAt?.toString() ?? 'N/A',
),
_buildDetailRow(
context,
'Resolution',
report.resolution ?? 'N/A',
),
_buildDetailRow(context, 'Account ID', report.accountId),
_buildDetailRow(
context,
'Created At',
report.createdAt.toString(),
),
_buildDetailRow(
context,
'Updated At',
report.updatedAt.toString(),
),
],
),
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
Widget _buildDetailRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.titleMedium).bold(),
Text(value, style: Theme.of(context).textTheme.bodyLarge),
],
),
);
}
}

View File

@ -0,0 +1,153 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/models/abuse_report_type.dart';
import 'package:island/services/abuse_report_service.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
class AbuseReportListScreen extends ConsumerStatefulWidget {
const AbuseReportListScreen({super.key});
@override
ConsumerState<AbuseReportListScreen> createState() =>
_AbuseReportListScreenState();
}
class _AbuseReportListScreenState extends ConsumerState<AbuseReportListScreen> {
Future<List<SnAbuseReport>>? _reportsFuture;
@override
void initState() {
super.initState();
_reportsFuture = ref.read(abuseReportServiceProvider).getReports();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: Text('abuseReports').tr()),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
showAbuseReportSheet(context, resourceIdentifier: 'unidentified');
},
),
body: FutureBuilder<List<SnAbuseReport>>(
future: _reportsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final reports = snapshot.data!;
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: reports.length,
itemBuilder: (context, index) {
final report = reports[index];
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: InkWell(
onTap: () {
context.push('/safety/reports/me/${report.id}');
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
report.reason,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.id,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Type',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
AbuseReportType.fromValue(
report.type,
).displayName,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Created at',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
'${report.createdAt.formatRelative(context)} · ${report.createdAt.formatSystem()}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.resolvedAt != null
? 'Resolved'
: 'Unresolved',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
report.resolvedAt != null
? Colors.green
: Colors.orange,
),
),
],
),
],
),
),
),
);
},
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/pods/network.dart';
final abuseReportServiceProvider = Provider<AbuseReportService>((ref) {
return AbuseReportService(ref);
});
class AbuseReportService {
final Ref ref;
AbuseReportService(this.ref);
Future<SnAbuseReport> getReport(String id) async {
final response =
await ref.read(apiClientProvider).get('/safety/reports/me/$id');
return SnAbuseReport.fromJson(response.data);
}
Future<List<SnAbuseReport>> getReports() async {
final response = await ref.read(apiClientProvider).get('/safety/reports/me');
return (response.data as List)
.map((json) => SnAbuseReport.fromJson(json))
.toList();
}
}

View File

@ -68,7 +68,6 @@ Future<void> subscribePushNotification(
bool detailedErrors = false,
}) async {
await FirebaseMessaging.instance.requestPermission(
provisional: true,
alert: true,
badge: true,
sound: true,

View File

@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/tour/techincal_review_intro.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tour.g.dart';
@ -12,7 +11,7 @@ part 'tour.freezed.dart';
const kAppTourStatusKey = "app_tour_statuses";
const List<Tour> kAllTours = [
Tour(id: 'technical_review_intro', isStartup: true),
// Tour(id: 'technical_review_intro', isStartup: true),
];
@freezed
@ -22,7 +21,7 @@ sealed class Tour with _$Tour {
const factory Tour({required String id, required bool isStartup}) = _Tour;
Widget get widget => switch (id) {
'technical_review_intro' => const TechicalReviewIntroWidget(),
// 'technical_review_intro' => const TechicalReviewIntroWidget(),
_ => throw UnimplementedError(),
};
}

View File

@ -0,0 +1,22 @@
String getAbuseReportTypeString(int type) {
switch (type) {
case 0:
return 'Copyright';
case 1:
return 'Harassment';
case 2:
return 'Impersonation';
case 3:
return 'Offensive Content';
case 4:
return 'Spam';
case 5:
return 'Privacy Violation';
case 6:
return 'Illegal Content';
case 7:
return 'Other';
default:
return 'Unknown';
}
}

View File

@ -384,7 +384,12 @@ class PostItem extends HookConsumerWidget {
// Show truncation hint if post is truncated
if (item.isTruncated && !isFullPost && item.type != 1)
_PostTruncateHint().padding(
bottom: item.attachments.isNotEmpty ? 8 : null,
bottom:
(item.attachments.isNotEmpty ||
item.repliedPost != null ||
item.forwardedPost != null)
? 8
: null,
),
if ((item.repliedPost != null ||
item.forwardedPost != null) &&
@ -571,7 +576,7 @@ class PostItem extends HookConsumerWidget {
callback: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'posts:${item.id}',
resourceIdentifier: 'post/${item.id}',
);
},
),

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.0.0+111
version: 3.0.0+112
environment:
sdk: ^3.7.2

View File

@ -0,0 +1,25 @@
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"W7HPZ53V6B.dev.solsynth.solian"
],
"paths": [
"*"
],
"components": [
{
"/": "/*"
}
]
}
]
},
"webcredentials": {
"apps": [
"W7HPZ53V6B.dev.solsynth.solian"
]
}
}

View File

@ -0,0 +1,12 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "dev.solsynth.solian",
"sha256_cert_fingerprints": [
"57:0C:A4:E6:1F:57:DF:56:70:42:05:4B:43:E2:DD:9E:00:E6:77:C3:D8:3C:5F:D5:A0:05:59:30:5A:85:F9:BC"
]
}
}
]