🐛 Dozen of hot fixes

This commit is contained in:
LittleSheep 2025-06-28 02:40:50 +08:00
parent 9e8f6d57df
commit ff475d43dd
11 changed files with 219 additions and 105 deletions

View File

@ -3,7 +3,7 @@ name: Build Release
on:
push:
tags:
- '*'
- "*"
workflow_dispatch:
jobs:
@ -59,6 +59,7 @@ jobs:
sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
sudo apt-get install -y libsecret-1
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
@ -80,4 +81,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'
path: "./*.AppImage*"

View File

@ -98,7 +98,7 @@
<receiver
android:name=".receiver.ReplyReceiver"
android:enabled="true"
android:exported="false" />
android:exported="true" />
<service
android:name=".service.MessagingService"

View File

@ -1,14 +1,39 @@
package dev.solsynth.solian
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
class MainActivity : FlutterActivity()
{
private val CHANNEL = "dev.solsynth.solian/notifications"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362
flutterEngine.plugins.add(LegacySharedPreferencesPlugin())
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "initialLink") {
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
result.success("/rooms/$roomId")
} else {
result.success(null)
}
} else {
result.notImplemented()
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANNEL).invokeMethod("newLink", "/rooms/$roomId")
}
}
}

View File

@ -1,48 +1,47 @@
package dev.solsynth.solian.network
import android.content.Context
import okhttp3.*
import android.content.SharedPreferences
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
import java.io.IOException
class ApiClient(private val context: Context) {
private val client = OkHttpClient()
private val sharedPreferences: SharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
fun sendMessage(roomId: String, content: String, repliedMessageId: String, callback: () -> Unit) {
val prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val token = prefs.getString("flutter.token", null)
val serverUrl = prefs.getString("flutter.serverUrl", null)
if (token == null || serverUrl == null) {
fun sendMessage(roomId: String, message: String, replyTo: String, callback: (Boolean) -> Unit) {
val token = sharedPreferences.getString("flutter.token", null)
if (token == null) {
callback(false)
return
}
val url = "$serverUrl/chat/$roomId/messages"
val json = JSONObject()
json.put("content", content)
json.put("replied_message_id", repliedMessageId)
val requestBody = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val json = JSONObject().apply {
put("content", message)
put("reply_to", replyTo)
}
val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request.Builder()
.url(url)
.post(requestBody)
.addHeader("Authorization", "AtField $token")
.url("https://solian.dev/api/rooms/$roomId/messages")
.header("Authorization", "Bearer $token")
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// Handle failure
callback()
callback(false)
}
override fun onResponse(call: Call, response: Response) {
// Handle success
callback()
callback(response.isSuccessful)
}
})
}
}
}

View File

@ -72,6 +72,7 @@ class MessagingService: FirebaseMessagingService() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra("room_id", roomId)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags)
val notificationBuilder = NotificationCompat.Builder(this, "messages")

View File

@ -89,14 +89,32 @@
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorPin": "Pin Code",
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
"realms": "Realms",
"createRealm": "Create a Realm",
"createRealmHint": "Meet friends with same interests, build communities, and more.",
"editRealm": "Edit Realm",
"deleteRealm": "Delete Realm",
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
"explore": "Explore",
"exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends",
"discover": "Discover",
"joinRealm": "Join Realm",
"account": "Account",
"name": "Name",
"slug": "Slug",
"slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
"createChatRoom": "Create a Room",
"editChatRoom": "Edit Room",
"deleteChatRoom": "Delete Room",
"deleteChatRoomHint": "Are you sure to delete this room? This action cannot be undone.",
"chat": "Chat",
"chatTabAll": "All",
"chatTabDirect": "Direct Messages",
"chatTabGroup": "Group Chats",
"chatMessageHint": "Message in {}",
"chatDirectMessageHint": "Message to {}",
"directMessage": "Direct Message",
"loading": "Loading...",
"descriptionNone": "No description yet.",
"invites": "Invites",
@ -231,6 +249,7 @@
"uploadingProgress": "Uploading {} of {}",
"uploadAll": "Upload All",
"stickerCopyPlaceholder": "Copy Placeholder",
"realmSelection": "Select a Realm",
"individual": "Individual",
"firstPostBadgeName": "First Post",
"firstPostBadgeDescription": "Created your first post on Solar Network",
@ -286,6 +305,10 @@
"levelingProgressExperience": "{} EXP",
"levelingProgressLevel": "Level {}",
"fileUploadingProgress": "Uploading file #{}: {}%",
"removeChatMember": "Remove Chat Room Member",
"removeChatMemberHint": "Are you sure to remove this member from the room?",
"removeRealmMember": "Remove Realm Member",
"removeRealmMemberHint": "Are you sure to remove this member from the realm?",
"memberRole": "Member Role",
"memberRoleHint": "Greater number has higher permission.",
"memberRoleEdit": "Edit role for @{}",
@ -293,6 +316,10 @@
"openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.",
"brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
"copyToClipboard": "Copy to clipboard",
"leaveChatRoom": "Leave Chat Room",
"leaveChatRoomHint": "Are you sure to leave this chat room?",
"leaveRealm": "Leave Realm",
"leaveRealmHint": "Are you sure to leave this realm?",
"walletNotFound": "Wallet not found",
"walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
"walletCreate": "Create a Wallet",
@ -304,6 +331,12 @@
"settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
},
"chatBreakNone": "None",
"settingsRealmCompactView": "Compact Realm View",
"settingsMixedFeed": "Mixed Feed",
"settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation",
@ -346,6 +379,7 @@
"postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private",
"postTruncated": "Content truncated, tap to view full post",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor",
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
@ -373,6 +407,10 @@
"lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"authDeviceEditLabel": "Edit Label",
"authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device",
@ -439,6 +477,21 @@
"contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"chatNotifyLevel": "Notify Level",
"chatNotifyLevelDescription": "Decide how many notifications you will receive.",
"chatNotifyLevelAll": "All",
"chatNotifyLevelMention": "Mentions",
"chatNotifyLevelNone": "None",
"chatNotifyLevelUpdated": "The notify level has been updated to {}.",
"chatBreak": "Take a Break",
"chatBreakDescription": "Set a time, before that time, your notification level will be metions only, to take a break of the current topic they're talking about.",
"chatBreakClear": "Clear the break time",
"chatBreakHour": "{} break",
"chatBreakDay": "{} day break",
"chatBreakSet": "Break set for {}",
"chatBreakCleared": "Chat break has been cleared.",
"chatBreakCustom": "Custom duration",
"chatBreakEnterMinutes": "Enter minutes",
"firstName": "First Name",
"middleName": "Middle Name",
"lastName": "Last Name",
@ -520,17 +573,29 @@
"quickActions": "Quick Actions",
"post": "Post",
"copy": "Copy",
"sendToChat": "Send to Chat",
"failedToShareToPost": "Failed to share to post: {}",
"shareToChatComingSoon": "Share to chat functionality coming soon",
"failedToShareToChat": "Failed to share to chat: {}",
"shareToSpecificChatComingSoon": "Share to {} coming soon",
"directChat": "Direct Chat",
"systemShareComingSoon": "System share functionality coming soon",
"failedToShareToSystem": "Failed to share to system: {}",
"failedToCopy": "Failed to copy: {}",
"noChatRoomsAvailable": "No chat rooms available",
"failedToLoadChats": "Failed to load chats",
"contentToShare": "Content to share:",
"unknownChat": "Unknown Chat",
"addAdditionalMessage": "Add additional message...",
"uploadingFiles": "Uploading files...",
"sharedSuccessfully": "Shared successfully!",
"shareSuccess": "Shared successfully!",
"shareToSpecificChatSuccess": "Shared to {} successfully!",
"wouldYouLikeToGoToChat": "Would you like to go to the chat?",
"no": "No",
"yes": "Yes",
"navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?",
"abuseReport": "Report",
"abuseReportTitle": "Report Content",
"abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",

View File

@ -7,6 +7,7 @@ import 'package:firebase_core/firebase_core.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker_android/image_picker_android.dart';
@ -158,6 +159,28 @@ class IslandApp extends HookConsumerWidget {
}
useEffect(() {
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 || Platform.isIOS)) {
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) {
if (message != null) {

View File

@ -79,33 +79,37 @@ final routerProvider = Provider<GoRouter>((ref) {
return EventCalanderScreen(name: name);
},
),
GoRoute(
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
ShellRoute(
builder:
(context, state, child) => CreatorHubShellScreen(child: child),
routes: [
GoRoute(
path: ':name/posts',
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
),
GoRoute(
path: '/creators/:name/posts',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPostListScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers',
path: '/creators/:name/stickers',
builder: (context, state) {
final name = state.pathParameters['name']!;
return StickersScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/new',
path: '/creators/:name/stickers/new',
builder: (context, state) {
final name = state.pathParameters['name']!;
return NewStickerPacksScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/:packId/edit',
path: '/creators/:name/stickers/:packId/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -113,7 +117,7 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId',
path: '/creators/:name/stickers/:packId',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -121,14 +125,14 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId/new',
path: '/creators/:name/stickers/:packId/new',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
return NewStickersScreen(packId: packId);
},
),
GoRoute(
path: ':name/stickers/:packId/:id/edit',
path: '/creators/:name/stickers/:packId/:id/edit',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
final id = state.pathParameters['id']!;
@ -136,11 +140,11 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: 'new',
path: '/creators/new',
builder: (context, state) => const NewPublisherScreen(),
),
GoRoute(
path: ':name/edit',
path: '/creators/:name/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
return EditPublisherScreen(name: name);
@ -173,56 +177,64 @@ final routerProvider = Provider<GoRouter>((ref) {
},
routes: [
// Explore tab
GoRoute(
path: '/',
builder: (context, state) => const ExploreScreen(),
ShellRoute(
builder:
(context, state, child) => ExploreShellScreen(child: child),
routes: [
GoRoute(
path: 'posts/:id',
path: '/',
builder: (context, state) => const ExploreScreen(),
),
GoRoute(
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
),
GoRoute(
path: 'publishers/:name',
path: '/publishers/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return PublisherProfileScreen(name: name);
},
),
GoRoute(
path: 'discovery/realms',
path: '/discovery/realms',
builder: (context, state) => const DiscoveryRealmsScreen(),
),
],
),
// Chat tab
GoRoute(
path: '/chat',
builder: (context, state) => const ChatListScreen(),
ShellRoute(
builder:
(context, state, child) => ChatShellScreen(child: child),
routes: [
GoRoute(
path: 'new',
path: '/chat',
builder: (context, state) => const ChatListScreen(),
),
GoRoute(
path: '/chat/new',
builder: (context, state) => const NewChatScreen(),
),
GoRoute(
path: ':id',
path: '/chat/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatRoomScreen(id: id);
},
),
GoRoute(
path: ':id/edit',
path: '/chat/:id/edit',
builder: (context, state) {
final id = state.pathParameters['id']!;
return EditChatScreen(id: id);
},
),
GoRoute(
path: ':id/detail',
path: '/chat/:id/detail',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatDetailScreen(id: id);
@ -258,39 +270,43 @@ final routerProvider = Provider<GoRouter>((ref) {
),
// Account tab
GoRoute(
path: '/account',
builder: (context, state) => const AccountScreen(),
ShellRoute(
builder:
(context, state, child) => AccountShellScreen(child: child),
routes: [
GoRoute(
path: 'notifications',
path: '/account',
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: '/account/notifications',
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: 'wallet',
path: '/account/wallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: 'relationships',
path: '/account/relationships',
builder: (context, state) => const RelationshipScreen(),
),
GoRoute(
path: ':name',
path: '/account/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name);
},
),
GoRoute(
path: 'me/update',
path: '/account/me/update',
builder: (context, state) => const UpdateProfileScreen(),
),
GoRoute(
path: 'me/leveling',
path: '/account/me/leveling',
builder: (context, state) => const LevelingScreen(),
),
GoRoute(
path: 'settings',
path: '/account/settings',
builder: (context, state) => const AccountSettingsScreen(),
),
],

View File

@ -143,7 +143,7 @@ class AccountScreen extends HookConsumerWidget {
progress: user.value!.profile.levelingProgress,
),
onTap: () {
context.push('/account/leveling');
context.push('/account/me/leveling');
},
).padding(horizontal: 12),
Row(
@ -210,7 +210,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('wallet').tr(),
onTap: () {
context.push('/wallet');
context.push('/account/wallet');
},
),
ListTile(

View File

@ -53,17 +53,21 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
@riverpod
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
try {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) {
return null;
}
}
@riverpod

View File

@ -4,19 +4,18 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/route.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:relative_time/relative_time.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:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'notification.g.dart';
@ -180,36 +179,17 @@ class NotificationScreen extends HookConsumerWidget {
),
),
onTap: () {
if (notification.meta['link'] is String) {
final href = notification.meta['link'];
final uri = Uri.tryParse(href);
if (uri == null) {
showSnackBar(
'brokenLink'.tr(args: []),
action: SnackBarAction(
label: 'copyToClipboard'.tr(),
onPressed: () {
Clipboard.setData(ClipboardData(text: href));
clearSnackBar(context);
},
),
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
return;
} else {
// External URLs
launchUrlString(uri);
}
if (uri.scheme == 'solian') {
context.push(
['', uri.host, ...uri.pathSegments].join('/'),
);
return;
}
showConfirmAlert(
'openLinkConfirmDescription'.tr(args: [href]),
'openLinkConfirm'.tr(),
).then((value) {
if (value) {
launchUrl(uri, mode: LaunchMode.externalApplication);
}
});
}
},
);