Compare commits
	
		
			8 Commits
		
	
	
		
			5939a1dc5b
			...
			3.0.0+109
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fe8640a6db | |||
| ff475d43dd | |||
| 9e8f6d57df | |||
| 79227a12e2 | |||
| a23dcfe702 | |||
| 243ecb3f71 | |||
| b8dec9f798 | |||
| 536375729f | 
							
								
								
									
										5
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,7 +3,7 @@ name: Build Release
 | 
				
			|||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    tags:
 | 
					    tags:
 | 
				
			||||||
      - '*'
 | 
					      - "*"
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
@@ -59,6 +59,7 @@ jobs:
 | 
				
			|||||||
          sudo apt-get install -y libnotify-dev
 | 
					          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 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
 | 
				
			||||||
          sudo apt-get install -y gstreamer-1.0
 | 
					          sudo apt-get install -y gstreamer-1.0
 | 
				
			||||||
 | 
					          sudo apt-get install -y libsecret-1
 | 
				
			||||||
      - run: flutter pub get
 | 
					      - run: flutter pub get
 | 
				
			||||||
      - run: flutter build linux
 | 
					      - run: flutter build linux
 | 
				
			||||||
      - name: Archive production artifacts
 | 
					      - name: Archive production artifacts
 | 
				
			||||||
@@ -80,4 +81,4 @@ jobs:
 | 
				
			|||||||
        uses: actions/upload-artifact@v4
 | 
					        uses: actions/upload-artifact@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: build-output-linux-appimage
 | 
					          name: build-output-linux-appimage
 | 
				
			||||||
          path: './*.AppImage*'
 | 
					          path: "./*.AppImage*"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,6 +57,9 @@ android {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
dependencies {
 | 
					dependencies {
 | 
				
			||||||
    implementation("com.google.android.material:material:1.12.0")
 | 
					    implementation("com.google.android.material:material:1.12.0")
 | 
				
			||||||
 | 
					    implementation("com.github.bumptech.glide:glide:4.16.0")
 | 
				
			||||||
 | 
					    implementation("com.squareup.okhttp3:okhttp:4.12.0")
 | 
				
			||||||
 | 
					    implementation("com.google.firebase:firebase-messaging-ktx")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
flutter {
 | 
					flutter {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,12 +46,37 @@
 | 
				
			|||||||
            <intent-filter>
 | 
					            <intent-filter>
 | 
				
			||||||
                <action android:name="android.intent.action.SEND" />
 | 
					                <action android:name="android.intent.action.SEND" />
 | 
				
			||||||
                <category android:name="android.intent.category.DEFAULT" />
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
                <data android:mimeType="*/*" />
 | 
					                <data android:mimeType="image/*" />
 | 
				
			||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
            <intent-filter>
 | 
					            <intent-filter>
 | 
				
			||||||
                <action android:name="android.intent.action.SEND_MULTIPLE" />
 | 
					                <action android:name="android.intent.action.SEND_MULTIPLE" />
 | 
				
			||||||
                <category android:name="android.intent.category.DEFAULT" />
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
                <data android:mimeType="*/*" />
 | 
					                <data android:mimeType="image/*" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.SEND" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <data android:mimeType="video/*" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.SEND_MULTIPLE" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <data android:mimeType="video/*" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.SEND" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <data android:mimeType="text/*" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.SEND" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <data android:mimeType="application/*" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.SEND_MULTIPLE" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <data android:mimeType="application/*" />
 | 
				
			||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
        </activity>
 | 
					        </activity>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -70,6 +95,19 @@
 | 
				
			|||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
        </activity>
 | 
					        </activity>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <receiver
 | 
				
			||||||
 | 
					            android:name=".receiver.ReplyReceiver"
 | 
				
			||||||
 | 
					            android:enabled="true"
 | 
				
			||||||
 | 
					            android:exported="true" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <service
 | 
				
			||||||
 | 
					            android:name=".service.MessagingService"
 | 
				
			||||||
 | 
					            android:exported="false">
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="com.google.firebase.MESSAGING_EVENT" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					        </service>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <provider
 | 
					        <provider
 | 
				
			||||||
            android:name="androidx.core.content.FileProvider"
 | 
					            android:name="androidx.core.content.FileProvider"
 | 
				
			||||||
            android:authorities="dev.solsynth.solian.provider"
 | 
					            android:authorities="dev.solsynth.solian.provider"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
package dev.solsynth.solian
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import io.flutter.embedding.android.FlutterActivity
 | 
					 | 
				
			||||||
import io.flutter.embedding.engine.FlutterEngine
 | 
					 | 
				
			||||||
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MainActivity : FlutterActivity()
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
 | 
					 | 
				
			||||||
        super.configureFlutterEngine(flutterEngine)
 | 
					 | 
				
			||||||
        // https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362
 | 
					 | 
				
			||||||
        flutterEngine.plugins.add(LegacySharedPreferencesPlugin())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -0,0 +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")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					package dev.solsynth.solian.network
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Context
 | 
				
			||||||
 | 
					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, message: String, replyTo: String, callback: (Boolean) -> Unit) {
 | 
				
			||||||
 | 
					        val token = sharedPreferences.getString("flutter.token", null)
 | 
				
			||||||
 | 
					        if (token == null) {
 | 
				
			||||||
 | 
					            callback(false)
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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("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) {
 | 
				
			||||||
 | 
					                callback(false)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            override fun onResponse(call: Call, response: Response) {
 | 
				
			||||||
 | 
					                callback(response.isSuccessful)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					package dev.solsynth.solian.receiver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.app.NotificationManager
 | 
				
			||||||
 | 
					import android.content.BroadcastReceiver
 | 
				
			||||||
 | 
					import android.content.Context
 | 
				
			||||||
 | 
					import android.content.Intent
 | 
				
			||||||
 | 
					import androidx.core.app.RemoteInput
 | 
				
			||||||
 | 
					import dev.solsynth.solian.network.ApiClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReplyReceiver : BroadcastReceiver() {
 | 
				
			||||||
 | 
					    override fun onReceive(context: Context, intent: Intent) {
 | 
				
			||||||
 | 
					        val remoteInput = RemoteInput.getResultsFromIntent(intent)
 | 
				
			||||||
 | 
					        if (remoteInput != null) {
 | 
				
			||||||
 | 
					            val replyText = remoteInput.getCharSequence("key_text_reply").toString()
 | 
				
			||||||
 | 
					            val roomId = intent.getStringExtra("room_id")
 | 
				
			||||||
 | 
					            val messageId = intent.getStringExtra("message_id")
 | 
				
			||||||
 | 
					            val notificationId = intent.getIntExtra("notification_id", 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (roomId != null && messageId != null) {
 | 
				
			||||||
 | 
					                ApiClient(context).sendMessage(roomId, replyText, messageId) {
 | 
				
			||||||
 | 
					                    val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 | 
				
			||||||
 | 
					                    notificationManager.cancel(notificationId)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					package dev.solsynth.solian.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.app.PendingIntent
 | 
				
			||||||
 | 
					import android.content.Intent
 | 
				
			||||||
 | 
					import android.graphics.Bitmap
 | 
				
			||||||
 | 
					import android.graphics.drawable.Drawable
 | 
				
			||||||
 | 
					import android.os.Build
 | 
				
			||||||
 | 
					import androidx.core.app.NotificationCompat
 | 
				
			||||||
 | 
					import androidx.core.app.NotificationManagerCompat
 | 
				
			||||||
 | 
					import androidx.core.app.RemoteInput
 | 
				
			||||||
 | 
					import com.bumptech.glide.Glide
 | 
				
			||||||
 | 
					import com.bumptech.glide.request.target.CustomTarget
 | 
				
			||||||
 | 
					import com.bumptech.glide.request.transition.Transition
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.FirebaseMessagingService
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.RemoteMessage
 | 
				
			||||||
 | 
					import dev.solsynth.solian.MainActivity
 | 
				
			||||||
 | 
					import dev.solsynth.solian.receiver.ReplyReceiver
 | 
				
			||||||
 | 
					import org.json.JSONObject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MessagingService: FirebaseMessagingService() {
 | 
				
			||||||
 | 
					    override fun onMessageReceived(remoteMessage: RemoteMessage) {
 | 
				
			||||||
 | 
					        val type = remoteMessage.data["type"]
 | 
				
			||||||
 | 
					        if (type == "messages.new") {
 | 
				
			||||||
 | 
					            handleMessageNotification(remoteMessage)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Handle other notification types
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun handleMessageNotification(remoteMessage: RemoteMessage) {
 | 
				
			||||||
 | 
					        val data = remoteMessage.data
 | 
				
			||||||
 | 
					        val metaString = data["meta"] ?: return
 | 
				
			||||||
 | 
					        val meta = JSONObject(metaString)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val pfp = meta.optString("pfp", null)
 | 
				
			||||||
 | 
					        val roomId = meta.optString("room_id", null)
 | 
				
			||||||
 | 
					        val messageId = meta.optString("message_id", null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val notificationId = System.currentTimeMillis().toInt()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val replyLabel = "Reply"
 | 
				
			||||||
 | 
					        val remoteInput = RemoteInput.Builder("key_text_reply")
 | 
				
			||||||
 | 
					            .setLabel(replyLabel)
 | 
				
			||||||
 | 
					            .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val replyIntent = Intent(this, ReplyReceiver::class.java).apply {
 | 
				
			||||||
 | 
					            putExtra("room_id", roomId)
 | 
				
			||||||
 | 
					            putExtra("message_id", messageId)
 | 
				
			||||||
 | 
					            putExtra("notification_id", notificationId)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 | 
				
			||||||
 | 
					            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            PendingIntent.FLAG_UPDATE_CURRENT
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val replyPendingIntent = PendingIntent.getBroadcast(
 | 
				
			||||||
 | 
					            applicationContext,
 | 
				
			||||||
 | 
					            notificationId,
 | 
				
			||||||
 | 
					            replyIntent,
 | 
				
			||||||
 | 
					            pendingIntentFlags
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val action = NotificationCompat.Action.Builder(
 | 
				
			||||||
 | 
					            android.R.drawable.ic_menu_send,
 | 
				
			||||||
 | 
					            replyLabel,
 | 
				
			||||||
 | 
					            replyPendingIntent
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					            .addRemoteInput(remoteInput)
 | 
				
			||||||
 | 
					            .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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")
 | 
				
			||||||
 | 
					            .setSmallIcon(android.R.drawable.ic_dialog_info)
 | 
				
			||||||
 | 
					            .setContentTitle(remoteMessage.notification?.title)
 | 
				
			||||||
 | 
					            .setContentText(remoteMessage.notification?.body)
 | 
				
			||||||
 | 
					            .setPriority(NotificationCompat.PRIORITY_HIGH)
 | 
				
			||||||
 | 
					            .setContentIntent(pendingIntent)
 | 
				
			||||||
 | 
					            .addAction(action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (pfp != null) {
 | 
				
			||||||
 | 
					            Glide.with(applicationContext)
 | 
				
			||||||
 | 
					                .asBitmap()
 | 
				
			||||||
 | 
					                .load(pfp)
 | 
				
			||||||
 | 
					                .into(object : CustomTarget<Bitmap>() {
 | 
				
			||||||
 | 
					                    override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
 | 
				
			||||||
 | 
					                        notificationBuilder.setLargeIcon(resource)
 | 
				
			||||||
 | 
					                        NotificationManagerCompat.from(applicationContext).notify(notificationId, notificationBuilder.build())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    override fun onLoadCleared(placeholder: Drawable?) {}
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            NotificationManagerCompat.from(this).notify(notificationId, notificationBuilder.build())
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -89,14 +89,32 @@
 | 
				
			|||||||
  "authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
 | 
					  "authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
 | 
				
			||||||
  "authFactorPin": "Pin Code",
 | 
					  "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.",
 | 
					  "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",
 | 
					  "explore": "Explore",
 | 
				
			||||||
  "exploreFilterSubscriptions": "Subscriptions",
 | 
					  "exploreFilterSubscriptions": "Subscriptions",
 | 
				
			||||||
  "exploreFilterFriends": "Friends",
 | 
					  "exploreFilterFriends": "Friends",
 | 
				
			||||||
  "discover": "Discover",
 | 
					  "discover": "Discover",
 | 
				
			||||||
 | 
					  "joinRealm": "Join Realm",
 | 
				
			||||||
  "account": "Account",
 | 
					  "account": "Account",
 | 
				
			||||||
  "name": "Name",
 | 
					  "name": "Name",
 | 
				
			||||||
  "slug": "Slug",
 | 
					  "slug": "Slug",
 | 
				
			||||||
  "slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
 | 
					  "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...",
 | 
					  "loading": "Loading...",
 | 
				
			||||||
  "descriptionNone": "No description yet.",
 | 
					  "descriptionNone": "No description yet.",
 | 
				
			||||||
  "invites": "Invites",
 | 
					  "invites": "Invites",
 | 
				
			||||||
@@ -231,6 +249,7 @@
 | 
				
			|||||||
  "uploadingProgress": "Uploading {} of {}",
 | 
					  "uploadingProgress": "Uploading {} of {}",
 | 
				
			||||||
  "uploadAll": "Upload All",
 | 
					  "uploadAll": "Upload All",
 | 
				
			||||||
  "stickerCopyPlaceholder": "Copy Placeholder",
 | 
					  "stickerCopyPlaceholder": "Copy Placeholder",
 | 
				
			||||||
 | 
					  "realmSelection": "Select a Realm",
 | 
				
			||||||
  "individual": "Individual",
 | 
					  "individual": "Individual",
 | 
				
			||||||
  "firstPostBadgeName": "First Post",
 | 
					  "firstPostBadgeName": "First Post",
 | 
				
			||||||
  "firstPostBadgeDescription": "Created your first post on Solar Network",
 | 
					  "firstPostBadgeDescription": "Created your first post on Solar Network",
 | 
				
			||||||
@@ -286,6 +305,10 @@
 | 
				
			|||||||
  "levelingProgressExperience": "{} EXP",
 | 
					  "levelingProgressExperience": "{} EXP",
 | 
				
			||||||
  "levelingProgressLevel": "Level {}",
 | 
					  "levelingProgressLevel": "Level {}",
 | 
				
			||||||
  "fileUploadingProgress": "Uploading file #{}: {}%",
 | 
					  "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",
 | 
					  "memberRole": "Member Role",
 | 
				
			||||||
  "memberRoleHint": "Greater number has higher permission.",
 | 
					  "memberRoleHint": "Greater number has higher permission.",
 | 
				
			||||||
  "memberRoleEdit": "Edit role for @{}",
 | 
					  "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.",
 | 
					  "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...",
 | 
					  "brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
 | 
				
			||||||
  "copyToClipboard": "Copy to clipboard",
 | 
					  "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",
 | 
					  "walletNotFound": "Wallet not found",
 | 
				
			||||||
  "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
 | 
					  "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
 | 
				
			||||||
  "walletCreate": "Create a Wallet",
 | 
					  "walletCreate": "Create a Wallet",
 | 
				
			||||||
@@ -304,6 +331,12 @@
 | 
				
			|||||||
  "settingsBackgroundImageClear": "Clear Background Image",
 | 
					  "settingsBackgroundImageClear": "Clear Background Image",
 | 
				
			||||||
  "settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
 | 
					  "settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
 | 
				
			||||||
  "messageNone": "No content to display",
 | 
					  "messageNone": "No content to display",
 | 
				
			||||||
 | 
					  "unreadMessages": {
 | 
				
			||||||
 | 
					    "one": "{} unread message",
 | 
				
			||||||
 | 
					    "other": "{} unread messages"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "chatBreakNone": "None",
 | 
				
			||||||
 | 
					  "settingsRealmCompactView": "Compact Realm View",
 | 
				
			||||||
  "settingsMixedFeed": "Mixed Feed",
 | 
					  "settingsMixedFeed": "Mixed Feed",
 | 
				
			||||||
  "settingsAutoTranslate": "Auto Translate",
 | 
					  "settingsAutoTranslate": "Auto Translate",
 | 
				
			||||||
  "settingsHideBottomNav": "Hide Bottom Navigation",
 | 
					  "settingsHideBottomNav": "Hide Bottom Navigation",
 | 
				
			||||||
@@ -346,6 +379,7 @@
 | 
				
			|||||||
  "postVisibilityUnlisted": "Unlisted",
 | 
					  "postVisibilityUnlisted": "Unlisted",
 | 
				
			||||||
  "postVisibilityPrivate": "Private",
 | 
					  "postVisibilityPrivate": "Private",
 | 
				
			||||||
  "postTruncated": "Content truncated, tap to view full post",
 | 
					  "postTruncated": "Content truncated, tap to view full post",
 | 
				
			||||||
 | 
					  "copyMessage": "Copy Message",
 | 
				
			||||||
  "authFactor": "Authentication Factor",
 | 
					  "authFactor": "Authentication Factor",
 | 
				
			||||||
  "authFactorDelete": "Delete the Factor",
 | 
					  "authFactorDelete": "Delete the Factor",
 | 
				
			||||||
  "authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
 | 
					  "authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
 | 
				
			||||||
@@ -373,6 +407,10 @@
 | 
				
			|||||||
  "lastActiveAt": "Last active at {}",
 | 
					  "lastActiveAt": "Last active at {}",
 | 
				
			||||||
  "authDeviceLogout": "Logout",
 | 
					  "authDeviceLogout": "Logout",
 | 
				
			||||||
  "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
 | 
					  "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",
 | 
					  "authDeviceEditLabel": "Edit Label",
 | 
				
			||||||
  "authDeviceLabelTitle": "Edit Device Label",
 | 
					  "authDeviceLabelTitle": "Edit Device Label",
 | 
				
			||||||
  "authDeviceLabelHint": "Enter a name for this device",
 | 
					  "authDeviceLabelHint": "Enter a name for this device",
 | 
				
			||||||
@@ -439,6 +477,21 @@
 | 
				
			|||||||
  "contactMethodSetPrimary": "Set as Primary",
 | 
					  "contactMethodSetPrimary": "Set as Primary",
 | 
				
			||||||
  "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
 | 
					  "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.",
 | 
					  "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",
 | 
					  "firstName": "First Name",
 | 
				
			||||||
  "middleName": "Middle Name",
 | 
					  "middleName": "Middle Name",
 | 
				
			||||||
  "lastName": "Last Name",
 | 
					  "lastName": "Last Name",
 | 
				
			||||||
@@ -520,17 +573,29 @@
 | 
				
			|||||||
  "quickActions": "Quick Actions",
 | 
					  "quickActions": "Quick Actions",
 | 
				
			||||||
  "post": "Post",
 | 
					  "post": "Post",
 | 
				
			||||||
  "copy": "Copy",
 | 
					  "copy": "Copy",
 | 
				
			||||||
 | 
					  "sendToChat": "Send to Chat",
 | 
				
			||||||
  "failedToShareToPost": "Failed to share to post: {}",
 | 
					  "failedToShareToPost": "Failed to share to post: {}",
 | 
				
			||||||
  "shareToChatComingSoon": "Share to chat functionality coming soon",
 | 
					  "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",
 | 
					  "systemShareComingSoon": "System share functionality coming soon",
 | 
				
			||||||
  "failedToShareToSystem": "Failed to share to system: {}",
 | 
					  "failedToShareToSystem": "Failed to share to system: {}",
 | 
				
			||||||
  "failedToCopy": "Failed to copy: {}",
 | 
					  "failedToCopy": "Failed to copy: {}",
 | 
				
			||||||
 | 
					  "noChatRoomsAvailable": "No chat rooms available",
 | 
				
			||||||
 | 
					  "failedToLoadChats": "Failed to load chats",
 | 
				
			||||||
  "contentToShare": "Content to share:",
 | 
					  "contentToShare": "Content to share:",
 | 
				
			||||||
 | 
					  "unknownChat": "Unknown Chat",
 | 
				
			||||||
 | 
					  "addAdditionalMessage": "Add additional message...",
 | 
				
			||||||
  "uploadingFiles": "Uploading files...",
 | 
					  "uploadingFiles": "Uploading files...",
 | 
				
			||||||
 | 
					  "sharedSuccessfully": "Shared successfully!",
 | 
				
			||||||
  "shareSuccess": "Shared successfully!",
 | 
					  "shareSuccess": "Shared successfully!",
 | 
				
			||||||
 | 
					  "shareToSpecificChatSuccess": "Shared to {} successfully!",
 | 
				
			||||||
  "wouldYouLikeToGoToChat": "Would you like to go to the chat?",
 | 
					  "wouldYouLikeToGoToChat": "Would you like to go to the chat?",
 | 
				
			||||||
  "no": "No",
 | 
					  "no": "No",
 | 
				
			||||||
  "yes": "Yes",
 | 
					  "yes": "Yes",
 | 
				
			||||||
 | 
					  "navigateToChat": "Navigate to Chat",
 | 
				
			||||||
 | 
					  "wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?",
 | 
				
			||||||
  "abuseReport": "Report",
 | 
					  "abuseReport": "Report",
 | 
				
			||||||
  "abuseReportTitle": "Report Content",
 | 
					  "abuseReportTitle": "Report Content",
 | 
				
			||||||
  "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
 | 
					  "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
 | 
				
			||||||
@@ -556,5 +621,13 @@
 | 
				
			|||||||
  "tags": "Tags",
 | 
					  "tags": "Tags",
 | 
				
			||||||
  "tagsHint": "Enter tags, separated by commas",
 | 
					  "tagsHint": "Enter tags, separated by commas",
 | 
				
			||||||
  "categories": "Categories",
 | 
					  "categories": "Categories",
 | 
				
			||||||
  "categoriesHint": "Enter categories, separated by commas"
 | 
					  "categoriesHint": "Enter categories, separated by commas",
 | 
				
			||||||
 | 
					  "chatNotJoined": "You have not joined this chat yet.",
 | 
				
			||||||
 | 
					  "chatUnableJoin": "You can't join this chat due to it's access control settings.",
 | 
				
			||||||
 | 
					  "chatJoin": "Join the Chat",
 | 
				
			||||||
 | 
					  "realmJoin": "Join the Realm",
 | 
				
			||||||
 | 
					  "realmJoinSuccess": "Successfully joined the realm.",
 | 
				
			||||||
 | 
					  "discoverRealms": "Discover Realms",
 | 
				
			||||||
 | 
					  "discoverPublishers": "Discover Publishers",
 | 
				
			||||||
 | 
					  "search": "Search"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -857,7 +857,7 @@
 | 
				
			|||||||
				INFOPLIST_FILE = SolianShareExtension/Info.plist;
 | 
									INFOPLIST_FILE = SolianShareExtension/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 18.5;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
@@ -900,7 +900,7 @@
 | 
				
			|||||||
				INFOPLIST_FILE = SolianShareExtension/Info.plist;
 | 
									INFOPLIST_FILE = SolianShareExtension/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 18.5;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
@@ -940,7 +940,7 @@
 | 
				
			|||||||
				INFOPLIST_FILE = SolianShareExtension/Info.plist;
 | 
									INFOPLIST_FILE = SolianShareExtension/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 18.5;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
@@ -979,7 +979,7 @@
 | 
				
			|||||||
				INFOPLIST_FILE = SolianNotificationService/Info.plist;
 | 
									INFOPLIST_FILE = SolianNotificationService/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 18.5;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 15.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
@@ -1021,7 +1021,7 @@
 | 
				
			|||||||
				INFOPLIST_FILE = SolianNotificationService/Info.plist;
 | 
									INFOPLIST_FILE = SolianNotificationService/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 18.5;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 15.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
@@ -1060,7 +1060,7 @@
 | 
				
			|||||||
				INFOPLIST_FILE = SolianNotificationService/Info.plist;
 | 
									INFOPLIST_FILE = SolianNotificationService/Info.plist;
 | 
				
			||||||
				INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
 | 
									INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
 | 
				
			||||||
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
									INFOPLIST_KEY_NSHumanReadableCopyright = "";
 | 
				
			||||||
				IPHONEOS_DEPLOYMENT_TARGET = 18.5;
 | 
									IPHONEOS_DEPLOYMENT_TARGET = 15.0;
 | 
				
			||||||
				LD_RUNPATH_SEARCH_PATHS = (
 | 
									LD_RUNPATH_SEARCH_PATHS = (
 | 
				
			||||||
					"$(inherited)",
 | 
										"$(inherited)",
 | 
				
			||||||
					"@executable_path/Frameworks",
 | 
										"@executable_path/Frameworks",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,21 @@ import UIKit
 | 
				
			|||||||
    ) -> Bool {
 | 
					    ) -> Bool {
 | 
				
			||||||
        UNUserNotificationCenter.current().delegate = notifyDelegate
 | 
					        UNUserNotificationCenter.current().delegate = notifyDelegate
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					        let replyableMessageCategory = UNNotificationCategory(
 | 
				
			||||||
 | 
					            identifier: "REPLYABLE_MESSAGE",
 | 
				
			||||||
 | 
					            actions: [
 | 
				
			||||||
 | 
					                UNTextInputNotificationAction(
 | 
				
			||||||
 | 
					                    identifier: "reply_action",
 | 
				
			||||||
 | 
					                    title: "Reply",
 | 
				
			||||||
 | 
					                    options: []
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            intentIdentifiers: [],
 | 
				
			||||||
 | 
					            options: []
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        GeneratedPluginRegistrant.register(with: self)
 | 
					        GeneratedPluginRegistrant.register(with: self)
 | 
				
			||||||
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
 | 
					        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,14 +10,26 @@ import Alamofire
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
 | 
					class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
 | 
				
			||||||
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
 | 
					    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
 | 
				
			||||||
        if let textResponse = response as? UNTextInputNotificationResponse {
 | 
					        guard let textResponse = response as? UNTextInputNotificationResponse else {
 | 
				
			||||||
            let content = response.notification.request.content
 | 
					            completionHandler()
 | 
				
			||||||
            guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else {
 | 
					 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var token: String? = UserDefaults.standard.getFlutterToken()
 | 
					        let content = response.notification.request.content
 | 
				
			||||||
            if token == nil {
 | 
					        
 | 
				
			||||||
 | 
					        // Only handle replies for new messages
 | 
				
			||||||
 | 
					        guard let notificationType = content.userInfo["type"] as? String, notificationType == "messages.new" else {
 | 
				
			||||||
 | 
					            completionHandler()
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else {
 | 
				
			||||||
 | 
					            completionHandler()
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        guard let token = UserDefaults.standard.getFlutterToken() else {
 | 
				
			||||||
 | 
					            completionHandler()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@@ -30,7 +42,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
 | 
					        AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
 | 
				
			||||||
                [HTTPHeader(name: "Authorization", value: "AtField \(token!)")]
 | 
					            [HTTPHeader(name: "Authorization", value: "AtField \(token)")]
 | 
				
			||||||
        ))
 | 
					        ))
 | 
				
			||||||
            .validate()
 | 
					            .validate()
 | 
				
			||||||
            .responseString { response in
 | 
					            .responseString { response in
 | 
				
			||||||
@@ -41,9 +53,8 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
 | 
				
			|||||||
                    print("Failed to send chat reply message: \(error)")
 | 
					                    print("Failed to send chat reply message: \(error)")
 | 
				
			||||||
                    break
 | 
					                    break
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                }
 | 
					                // Call completion handler after network request is finished
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
                completionHandler()
 | 
					                completionHandler()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,21 +60,7 @@ class NotificationService: UNNotificationServiceExtension {
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        let pfpIdentifier = meta["pfp"] as? String
 | 
					        let pfpIdentifier = meta["pfp"] as? String
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        let replyableMessageCategory = UNNotificationCategory(
 | 
					        content.categoryIdentifier = "REPLYABLE_MESSAGE"
 | 
				
			||||||
            identifier: content.categoryIdentifier,
 | 
					 | 
				
			||||||
            actions: [
 | 
					 | 
				
			||||||
                UNTextInputNotificationAction(
 | 
					 | 
				
			||||||
                    identifier: "reply_action",
 | 
					 | 
				
			||||||
                    title: "Reply",
 | 
					 | 
				
			||||||
                    options: []
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            intentIdentifiers: [],
 | 
					 | 
				
			||||||
            options: []
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
 | 
					 | 
				
			||||||
        content.categoryIdentifier = replyableMessageCategory.identifier
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        let metaCopy = meta as? [String: Any] ?? [:]
 | 
					        let metaCopy = meta as? [String: Any] ?? [:]
 | 
				
			||||||
        let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil
 | 
					        let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,25 +71,32 @@ class MessageRepository {
 | 
				
			|||||||
    bool synced = false,
 | 
					    bool synced = false,
 | 
				
			||||||
  }) async {
 | 
					  }) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      // For initial load, fetch latest messages in the background to sync.
 | 
				
			||||||
 | 
					      if (offset == 0 && !synced) {
 | 
				
			||||||
 | 
					        // Not awaiting this is intentional, for a quicker UI response.
 | 
				
			||||||
 | 
					        // The UI should rely on a stream from the database to get updates.
 | 
				
			||||||
 | 
					        _fetchAndCacheMessages(room.id, offset: 0, take: take).catchError((_) {
 | 
				
			||||||
 | 
					          // Best effort, errors will be handled by later fetches.
 | 
				
			||||||
 | 
					          return <LocalChatMessage>[];
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      final localMessages = await _getCachedMessages(
 | 
					      final localMessages = await _getCachedMessages(
 | 
				
			||||||
        room.id,
 | 
					        room.id,
 | 
				
			||||||
        offset: offset,
 | 
					        offset: offset,
 | 
				
			||||||
        take: take,
 | 
					        take: take,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // If it already synced with the remote, skip this
 | 
					      // If local cache has messages, return them. This is the common case for scrolling up.
 | 
				
			||||||
      if (offset == 0 && !synced) {
 | 
					 | 
				
			||||||
        // Fetch latest messages
 | 
					 | 
				
			||||||
        _fetchAndCacheMessages(room.id, offset: offset, take: take);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (localMessages.isNotEmpty) {
 | 
					      if (localMessages.isNotEmpty) {
 | 
				
			||||||
        return localMessages;
 | 
					        return localMessages;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // If local cache is empty, we've probably reached the end of cached history.
 | 
				
			||||||
 | 
					      // Fetch from remote. This will also be hit on first load if cache is empty.
 | 
				
			||||||
      return await _fetchAndCacheMessages(room.id, offset: offset, take: take);
 | 
					      return await _fetchAndCacheMessages(room.id, offset: offset, take: take);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      // If API fails but we have local messages, return them
 | 
					      // Final fallback to cache in case of network errors during fetch.
 | 
				
			||||||
      final localMessages = await _getCachedMessages(
 | 
					      final localMessages = await _getCachedMessages(
 | 
				
			||||||
        room.id,
 | 
					        room.id,
 | 
				
			||||||
        offset: offset,
 | 
					        offset: offset,
 | 
				
			||||||
@@ -117,24 +124,26 @@ class MessageRepository {
 | 
				
			|||||||
    final dbLocalMessages =
 | 
					    final dbLocalMessages =
 | 
				
			||||||
        dbMessages.map(_database.companionToMessage).toList();
 | 
					        dbMessages.map(_database.companionToMessage).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Combine with pending messages
 | 
					    // Combine with pending messages for the first page
 | 
				
			||||||
 | 
					    if (offset == 0) {
 | 
				
			||||||
      final pendingForRoom =
 | 
					      final pendingForRoom =
 | 
				
			||||||
          pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
 | 
					          pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Sort by timestamp descending (newest first)
 | 
					 | 
				
			||||||
      final allMessages = [...pendingForRoom, ...dbLocalMessages];
 | 
					      final allMessages = [...pendingForRoom, ...dbLocalMessages];
 | 
				
			||||||
      allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
 | 
					      allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Apply pagination
 | 
					      // Remove duplicates by ID, preserving the order
 | 
				
			||||||
    if (offset >= allMessages.length) {
 | 
					      final uniqueMessages = <LocalChatMessage>[];
 | 
				
			||||||
      return [];
 | 
					      final seenIds = <String>{};
 | 
				
			||||||
 | 
					      for (final message in allMessages) {
 | 
				
			||||||
 | 
					        if (seenIds.add(message.id)) {
 | 
				
			||||||
 | 
					          uniqueMessages.add(message);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return uniqueMessages;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final end =
 | 
					    return dbLocalMessages;
 | 
				
			||||||
        (offset + take) > allMessages.length
 | 
					 | 
				
			||||||
            ? allMessages.length
 | 
					 | 
				
			||||||
            : (offset + take);
 | 
					 | 
				
			||||||
    return allMessages.sublist(offset, end);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<List<LocalChatMessage>> _fetchAndCacheMessages(
 | 
					  Future<List<LocalChatMessage>> _fetchAndCacheMessages(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import 'package:firebase_core/firebase_core.dart';
 | 
				
			|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
					import 'package:firebase_messaging/firebase_messaging.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:image_picker_android/image_picker_android.dart';
 | 
					import 'package:image_picker_android/image_picker_android.dart';
 | 
				
			||||||
@@ -158,6 +159,28 @@ class IslandApp extends HookConsumerWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    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.
 | 
					      // When the app is opened from a terminated state.
 | 
				
			||||||
      FirebaseMessaging.instance.getInitialMessage().then((message) {
 | 
					      FirebaseMessaging.instance.getInitialMessage().then((message) {
 | 
				
			||||||
        if (message != null) {
 | 
					        if (message != null) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,8 @@ sealed class SnChatRoom with _$SnChatRoom {
 | 
				
			|||||||
    required String? name,
 | 
					    required String? name,
 | 
				
			||||||
    required String? description,
 | 
					    required String? description,
 | 
				
			||||||
    required int type,
 | 
					    required int type,
 | 
				
			||||||
    required bool isPublic,
 | 
					    @Default(false) bool isPublic,
 | 
				
			||||||
    required bool isCommunity,
 | 
					    @Default(false) bool isCommunity,
 | 
				
			||||||
    required SnCloudFile? picture,
 | 
					    required SnCloudFile? picture,
 | 
				
			||||||
    required SnCloudFile? background,
 | 
					    required SnCloudFile? background,
 | 
				
			||||||
    required String? realmId,
 | 
					    required String? realmId,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -129,15 +129,15 @@ $SnRealmCopyWith<$Res>? get realm {
 | 
				
			|||||||
@JsonSerializable()
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _SnChatRoom implements SnChatRoom {
 | 
					class _SnChatRoom implements SnChatRoom {
 | 
				
			||||||
  const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.isCommunity, required this.picture, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final  List<SnChatMember>? members}): _members = members;
 | 
					  const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, this.isPublic = false, this.isCommunity = false, required this.picture, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final  List<SnChatMember>? members}): _members = members;
 | 
				
			||||||
  factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
 | 
					  factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  String id;
 | 
					@override final  String id;
 | 
				
			||||||
@override final  String? name;
 | 
					@override final  String? name;
 | 
				
			||||||
@override final  String? description;
 | 
					@override final  String? description;
 | 
				
			||||||
@override final  int type;
 | 
					@override final  int type;
 | 
				
			||||||
@override final  bool isPublic;
 | 
					@override@JsonKey() final  bool isPublic;
 | 
				
			||||||
@override final  bool isCommunity;
 | 
					@override@JsonKey() final  bool isCommunity;
 | 
				
			||||||
@override final  SnCloudFile? picture;
 | 
					@override final  SnCloudFile? picture;
 | 
				
			||||||
@override final  SnCloudFile? background;
 | 
					@override final  SnCloudFile? background;
 | 
				
			||||||
@override final  String? realmId;
 | 
					@override final  String? realmId;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,8 +11,8 @@ _SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom(
 | 
				
			|||||||
  name: json['name'] as String?,
 | 
					  name: json['name'] as String?,
 | 
				
			||||||
  description: json['description'] as String?,
 | 
					  description: json['description'] as String?,
 | 
				
			||||||
  type: (json['type'] as num).toInt(),
 | 
					  type: (json['type'] as num).toInt(),
 | 
				
			||||||
  isPublic: json['is_public'] as bool,
 | 
					  isPublic: json['is_public'] as bool? ?? false,
 | 
				
			||||||
  isCommunity: json['is_community'] as bool,
 | 
					  isCommunity: json['is_community'] as bool? ?? false,
 | 
				
			||||||
  picture:
 | 
					  picture:
 | 
				
			||||||
      json['picture'] == null
 | 
					      json['picture'] == null
 | 
				
			||||||
          ? null
 | 
					          ? null
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ sealed class SnRealm with _$SnRealm {
 | 
				
			|||||||
  const factory SnRealm({
 | 
					  const factory SnRealm({
 | 
				
			||||||
    required String id,
 | 
					    required String id,
 | 
				
			||||||
    required String slug,
 | 
					    required String slug,
 | 
				
			||||||
    required String name,
 | 
					    @Default('') String name,
 | 
				
			||||||
    @Default('') String description,
 | 
					    @Default('') String description,
 | 
				
			||||||
    required String? verifiedAs,
 | 
					    required String? verifiedAs,
 | 
				
			||||||
    required DateTime? verifiedAt,
 | 
					    required DateTime? verifiedAt,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -117,12 +117,12 @@ $SnCloudFileCopyWith<$Res>? get background {
 | 
				
			|||||||
@JsonSerializable()
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _SnRealm implements SnRealm {
 | 
					class _SnRealm implements SnRealm {
 | 
				
			||||||
  const _SnRealm({required this.id, required this.slug, required this.name, this.description = '', required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.picture, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
 | 
					  const _SnRealm({required this.id, required this.slug, this.name = '', this.description = '', required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.picture, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
 | 
				
			||||||
  factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json);
 | 
					  factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  String id;
 | 
					@override final  String id;
 | 
				
			||||||
@override final  String slug;
 | 
					@override final  String slug;
 | 
				
			||||||
@override final  String name;
 | 
					@override@JsonKey() final  String name;
 | 
				
			||||||
@override@JsonKey() final  String description;
 | 
					@override@JsonKey() final  String description;
 | 
				
			||||||
@override final  String? verifiedAs;
 | 
					@override final  String? verifiedAs;
 | 
				
			||||||
@override final  DateTime? verifiedAt;
 | 
					@override final  DateTime? verifiedAt;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ part of 'realm.dart';
 | 
				
			|||||||
_SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm(
 | 
					_SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm(
 | 
				
			||||||
  id: json['id'] as String,
 | 
					  id: json['id'] as String,
 | 
				
			||||||
  slug: json['slug'] as String,
 | 
					  slug: json['slug'] as String,
 | 
				
			||||||
  name: json['name'] as String,
 | 
					  name: json['name'] as String? ?? '',
 | 
				
			||||||
  description: json['description'] as String? ?? '',
 | 
					  description: json['description'] as String? ?? '',
 | 
				
			||||||
  verifiedAs: json['verified_as'] as String?,
 | 
					  verifiedAs: json['verified_as'] as String?,
 | 
				
			||||||
  verifiedAt:
 | 
					  verifiedAt:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
 | 
				
			|||||||
    state = const AsyncValue.data(null);
 | 
					    state = const AsyncValue.data(null);
 | 
				
			||||||
    final prefs = _ref.read(sharedPreferencesProvider);
 | 
					    final prefs = _ref.read(sharedPreferencesProvider);
 | 
				
			||||||
    await prefs.remove(kTokenPairStoreKey);
 | 
					    await prefs.remove(kTokenPairStoreKey);
 | 
				
			||||||
    _ref.invalidate(userInfoProvider);
 | 
					 | 
				
			||||||
    _ref.invalidate(tokenProvider);
 | 
					    _ref.invalidate(tokenProvider);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,7 +53,10 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
          // Standalone routes without bottom navigation
 | 
					          // Standalone routes without bottom navigation
 | 
				
			||||||
          GoRoute(
 | 
					          GoRoute(
 | 
				
			||||||
            path: '/posts/compose',
 | 
					            path: '/posts/compose',
 | 
				
			||||||
            builder: (context, state) => const PostComposeScreen(),
 | 
					            builder:
 | 
				
			||||||
 | 
					                (context, state) => PostComposeScreen(
 | 
				
			||||||
 | 
					                  initialState: state.extra as PostComposeInitialState?,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          GoRoute(
 | 
					          GoRoute(
 | 
				
			||||||
            path: '/posts/:id/edit',
 | 
					            path: '/posts/:id/edit',
 | 
				
			||||||
@@ -76,33 +79,37 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
              return EventCalanderScreen(name: name);
 | 
					              return EventCalanderScreen(name: name);
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					          ShellRoute(
 | 
				
			||||||
 | 
					            builder:
 | 
				
			||||||
 | 
					                (context, state, child) => CreatorHubShellScreen(child: child),
 | 
				
			||||||
 | 
					            routes: [
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: '/creators',
 | 
					                path: '/creators',
 | 
				
			||||||
                builder: (context, state) => const CreatorHubScreen(),
 | 
					                builder: (context, state) => const CreatorHubScreen(),
 | 
				
			||||||
            routes: [
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/posts',
 | 
					                path: '/creators/:name/posts',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final name = state.pathParameters['name']!;
 | 
					                  final name = state.pathParameters['name']!;
 | 
				
			||||||
                  return CreatorPostListScreen(pubName: name);
 | 
					                  return CreatorPostListScreen(pubName: name);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/stickers',
 | 
					                path: '/creators/:name/stickers',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final name = state.pathParameters['name']!;
 | 
					                  final name = state.pathParameters['name']!;
 | 
				
			||||||
                  return StickersScreen(pubName: name);
 | 
					                  return StickersScreen(pubName: name);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/stickers/new',
 | 
					                path: '/creators/:name/stickers/new',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final name = state.pathParameters['name']!;
 | 
					                  final name = state.pathParameters['name']!;
 | 
				
			||||||
                  return NewStickerPacksScreen(pubName: name);
 | 
					                  return NewStickerPacksScreen(pubName: name);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/stickers/:packId/edit',
 | 
					                path: '/creators/:name/stickers/:packId/edit',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final name = state.pathParameters['name']!;
 | 
					                  final name = state.pathParameters['name']!;
 | 
				
			||||||
                  final packId = state.pathParameters['packId']!;
 | 
					                  final packId = state.pathParameters['packId']!;
 | 
				
			||||||
@@ -110,7 +117,7 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/stickers/:packId',
 | 
					                path: '/creators/:name/stickers/:packId',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final name = state.pathParameters['name']!;
 | 
					                  final name = state.pathParameters['name']!;
 | 
				
			||||||
                  final packId = state.pathParameters['packId']!;
 | 
					                  final packId = state.pathParameters['packId']!;
 | 
				
			||||||
@@ -118,14 +125,14 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/stickers/:packId/new',
 | 
					                path: '/creators/:name/stickers/:packId/new',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final packId = state.pathParameters['packId']!;
 | 
					                  final packId = state.pathParameters['packId']!;
 | 
				
			||||||
                  return NewStickersScreen(packId: packId);
 | 
					                  return NewStickersScreen(packId: packId);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/stickers/:packId/:id/edit',
 | 
					                path: '/creators/:name/stickers/:packId/:id/edit',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final packId = state.pathParameters['packId']!;
 | 
					                  final packId = state.pathParameters['packId']!;
 | 
				
			||||||
                  final id = state.pathParameters['id']!;
 | 
					                  final id = state.pathParameters['id']!;
 | 
				
			||||||
@@ -133,11 +140,11 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: 'new',
 | 
					                path: '/creators/new',
 | 
				
			||||||
                builder: (context, state) => const NewPublisherScreen(),
 | 
					                builder: (context, state) => const NewPublisherScreen(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GoRoute(
 | 
					              GoRoute(
 | 
				
			||||||
                path: ':name/edit',
 | 
					                path: '/creators/:name/edit',
 | 
				
			||||||
                builder: (context, state) {
 | 
					                builder: (context, state) {
 | 
				
			||||||
                  final name = state.pathParameters['name']!;
 | 
					                  final name = state.pathParameters['name']!;
 | 
				
			||||||
                  return EditPublisherScreen(name: name);
 | 
					                  return EditPublisherScreen(name: name);
 | 
				
			||||||
@@ -170,56 +177,64 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            routes: [
 | 
					            routes: [
 | 
				
			||||||
              // Explore tab
 | 
					              // Explore tab
 | 
				
			||||||
 | 
					              ShellRoute(
 | 
				
			||||||
 | 
					                builder:
 | 
				
			||||||
 | 
					                    (context, state, child) => ExploreShellScreen(child: child),
 | 
				
			||||||
 | 
					                routes: [
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: '/',
 | 
					                    path: '/',
 | 
				
			||||||
                    builder: (context, state) => const ExploreScreen(),
 | 
					                    builder: (context, state) => const ExploreScreen(),
 | 
				
			||||||
                routes: [
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'posts/:id',
 | 
					                    path: '/posts/:id',
 | 
				
			||||||
                    builder: (context, state) {
 | 
					                    builder: (context, state) {
 | 
				
			||||||
                      final id = state.pathParameters['id']!;
 | 
					                      final id = state.pathParameters['id']!;
 | 
				
			||||||
                      return PostDetailScreen(id: id);
 | 
					                      return PostDetailScreen(id: id);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'publishers/:name',
 | 
					                    path: '/publishers/:name',
 | 
				
			||||||
                    builder: (context, state) {
 | 
					                    builder: (context, state) {
 | 
				
			||||||
                      final name = state.pathParameters['name']!;
 | 
					                      final name = state.pathParameters['name']!;
 | 
				
			||||||
                      return PublisherProfileScreen(name: name);
 | 
					                      return PublisherProfileScreen(name: name);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'discovery/realms',
 | 
					                    path: '/discovery/realms',
 | 
				
			||||||
                    builder: (context, state) => const DiscoveryRealmsScreen(),
 | 
					                    builder: (context, state) => const DiscoveryRealmsScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              // Chat tab
 | 
					              // Chat tab
 | 
				
			||||||
 | 
					              ShellRoute(
 | 
				
			||||||
 | 
					                builder:
 | 
				
			||||||
 | 
					                    (context, state, child) => ChatShellScreen(child: child),
 | 
				
			||||||
 | 
					                routes: [
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: '/chat',
 | 
					                    path: '/chat',
 | 
				
			||||||
                    builder: (context, state) => const ChatListScreen(),
 | 
					                    builder: (context, state) => const ChatListScreen(),
 | 
				
			||||||
                routes: [
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'new',
 | 
					                    path: '/chat/new',
 | 
				
			||||||
                    builder: (context, state) => const NewChatScreen(),
 | 
					                    builder: (context, state) => const NewChatScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: ':id',
 | 
					                    path: '/chat/:id',
 | 
				
			||||||
                    builder: (context, state) {
 | 
					                    builder: (context, state) {
 | 
				
			||||||
                      final id = state.pathParameters['id']!;
 | 
					                      final id = state.pathParameters['id']!;
 | 
				
			||||||
                      return ChatRoomScreen(id: id);
 | 
					                      return ChatRoomScreen(id: id);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: ':id/edit',
 | 
					                    path: '/chat/:id/edit',
 | 
				
			||||||
                    builder: (context, state) {
 | 
					                    builder: (context, state) {
 | 
				
			||||||
                      final id = state.pathParameters['id']!;
 | 
					                      final id = state.pathParameters['id']!;
 | 
				
			||||||
                      return EditChatScreen(id: id);
 | 
					                      return EditChatScreen(id: id);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: ':id/detail',
 | 
					                    path: '/chat/:id/detail',
 | 
				
			||||||
                    builder: (context, state) {
 | 
					                    builder: (context, state) {
 | 
				
			||||||
                      final id = state.pathParameters['id']!;
 | 
					                      final id = state.pathParameters['id']!;
 | 
				
			||||||
                      return ChatDetailScreen(id: id);
 | 
					                      return ChatDetailScreen(id: id);
 | 
				
			||||||
@@ -255,39 +270,43 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              // Account tab
 | 
					              // Account tab
 | 
				
			||||||
 | 
					              ShellRoute(
 | 
				
			||||||
 | 
					                builder:
 | 
				
			||||||
 | 
					                    (context, state, child) => AccountShellScreen(child: child),
 | 
				
			||||||
 | 
					                routes: [
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: '/account',
 | 
					                    path: '/account',
 | 
				
			||||||
                    builder: (context, state) => const AccountScreen(),
 | 
					                    builder: (context, state) => const AccountScreen(),
 | 
				
			||||||
                routes: [
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'notifications',
 | 
					                    path: '/account/notifications',
 | 
				
			||||||
                    builder: (context, state) => const NotificationScreen(),
 | 
					                    builder: (context, state) => const NotificationScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'wallet',
 | 
					                    path: '/account/wallet',
 | 
				
			||||||
                    builder: (context, state) => const WalletScreen(),
 | 
					                    builder: (context, state) => const WalletScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'relationships',
 | 
					                    path: '/account/relationships',
 | 
				
			||||||
                    builder: (context, state) => const RelationshipScreen(),
 | 
					                    builder: (context, state) => const RelationshipScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: ':name',
 | 
					                    path: '/account/:name',
 | 
				
			||||||
                    builder: (context, state) {
 | 
					                    builder: (context, state) {
 | 
				
			||||||
                      final name = state.pathParameters['name']!;
 | 
					                      final name = state.pathParameters['name']!;
 | 
				
			||||||
                      return AccountProfileScreen(name: name);
 | 
					                      return AccountProfileScreen(name: name);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'me/update',
 | 
					                    path: '/account/me/update',
 | 
				
			||||||
                    builder: (context, state) => const UpdateProfileScreen(),
 | 
					                    builder: (context, state) => const UpdateProfileScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'me/leveling',
 | 
					                    path: '/account/me/leveling',
 | 
				
			||||||
                    builder: (context, state) => const LevelingScreen(),
 | 
					                    builder: (context, state) => const LevelingScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    path: 'settings',
 | 
					                    path: '/account/settings',
 | 
				
			||||||
                    builder: (context, state) => const AccountSettingsScreen(),
 | 
					                    builder: (context, state) => const AccountSettingsScreen(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -143,7 +143,7 @@ class AccountScreen extends HookConsumerWidget {
 | 
				
			|||||||
                progress: user.value!.profile.levelingProgress,
 | 
					                progress: user.value!.profile.levelingProgress,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                context.push('/account/leveling');
 | 
					                context.push('/account/me/leveling');
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ).padding(horizontal: 12),
 | 
					            ).padding(horizontal: 12),
 | 
				
			||||||
            Row(
 | 
					            Row(
 | 
				
			||||||
@@ -210,7 +210,7 @@ class AccountScreen extends HookConsumerWidget {
 | 
				
			|||||||
              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
					              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
              title: Text('wallet').tr(),
 | 
					              title: Text('wallet').tr(),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                context.push('/wallet');
 | 
					                context.push('/account/wallet');
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,6 +53,7 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
 | 
					Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    final account = await ref.watch(accountProvider(uname).future);
 | 
					    final account = await ref.watch(accountProvider(uname).future);
 | 
				
			||||||
    if (account.profile.background == null) return null;
 | 
					    if (account.profile.background == null) return null;
 | 
				
			||||||
    final palette = await PaletteGenerator.fromImageProvider(
 | 
					    final palette = await PaletteGenerator.fromImageProvider(
 | 
				
			||||||
@@ -64,6 +65,9 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
 | 
				
			|||||||
    final dominantColor = palette.dominantColor?.color;
 | 
					    final dominantColor = palette.dominantColor?.color;
 | 
				
			||||||
    if (dominantColor == null) return null;
 | 
					    if (dominantColor == null) return null;
 | 
				
			||||||
    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
					    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
				
			||||||
 | 
					  } catch (_) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -434,17 +434,31 @@ class ChatListScreen extends HookConsumerWidget {
 | 
				
			|||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
Future<SnChatRoom?> chatroom(Ref ref, String? identifier) async {
 | 
					Future<SnChatRoom?> chatroom(Ref ref, String? identifier) async {
 | 
				
			||||||
  if (identifier == null) return null;
 | 
					  if (identifier == null) return null;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    final client = ref.watch(apiClientProvider);
 | 
					    final client = ref.watch(apiClientProvider);
 | 
				
			||||||
    final resp = await client.get('/chat/$identifier');
 | 
					    final resp = await client.get('/chat/$identifier');
 | 
				
			||||||
    return SnChatRoom.fromJson(resp.data);
 | 
					    return SnChatRoom.fromJson(resp.data);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    if (err is DioException && err.response?.statusCode == 404) {
 | 
				
			||||||
 | 
					      return null; // Chat room not found
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    rethrow; // Rethrow other errors
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async {
 | 
					Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async {
 | 
				
			||||||
  if (identifier == null) return null;
 | 
					  if (identifier == null) return null;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    final client = ref.watch(apiClientProvider);
 | 
					    final client = ref.watch(apiClientProvider);
 | 
				
			||||||
    final resp = await client.get('/chat/$identifier/members/me');
 | 
					    final resp = await client.get('/chat/$identifier/members/me');
 | 
				
			||||||
    return SnChatMember.fromJson(resp.data);
 | 
					    return SnChatMember.fromJson(resp.data);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    if (err is DioException && err.response?.statusCode == 404) {
 | 
				
			||||||
 | 
					      return null; // Chat member not found
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    rethrow; // Rethrow other errors
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NewChatScreen extends StatelessWidget {
 | 
					class NewChatScreen extends StatelessWidget {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ final chatroomsJoinedProvider =
 | 
				
			|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
 | 
					@Deprecated('Will be removed in 3.0. Use Ref instead')
 | 
				
			||||||
// ignore: unused_element
 | 
					// ignore: unused_element
 | 
				
			||||||
typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef<List<SnChatRoom>>;
 | 
					typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef<List<SnChatRoom>>;
 | 
				
			||||||
String _$chatroomHash() => r'dce3c0fc407f178bb7c306a08b9fa545795a9205';
 | 
					String _$chatroomHash() => r'8dac7aaac50932e6dd213039102d43c1cf5f1d4e';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Copied from Dart SDK
 | 
					/// Copied from Dart SDK
 | 
				
			||||||
class _SystemHash {
 | 
					class _SystemHash {
 | 
				
			||||||
@@ -164,7 +164,7 @@ class _ChatroomProviderElement
 | 
				
			|||||||
  String? get identifier => (origin as ChatroomProvider).identifier;
 | 
					  String? get identifier => (origin as ChatroomProvider).identifier;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$chatroomIdentityHash() => r'4c349ea4265df7b0498cf26c82dbaabe3d868727';
 | 
					String _$chatroomIdentityHash() => r'ad6ad09b6fc4cf7c4abe146ea97f8e364a3d4fd0';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// See also [chatroomIdentity].
 | 
					/// See also [chatroomIdentity].
 | 
				
			||||||
@ProviderFor(chatroomIdentity)
 | 
					@ProviderFor(chatroomIdentity)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -305,7 +305,55 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
      // Identity was not found, user was not joined
 | 
					      // Identity was not found, user was not joined
 | 
				
			||||||
      return AppScaffold(
 | 
					      return AppScaffold(
 | 
				
			||||||
        appBar: AppBar(leading: const PageBackButton()),
 | 
					        appBar: AppBar(leading: const PageBackButton()),
 | 
				
			||||||
        body: Center(child: Text('You are not a member of this chat room')),
 | 
					        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(
 | 
				
			||||||
 | 
					                              '/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(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -443,6 +491,28 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
      return () => subscription.cancel();
 | 
					      return () => subscription.cancel();
 | 
				
			||||||
    }, [ws, chatRoom]);
 | 
					    }, [ws, chatRoom]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      final wsState = ref.read(websocketStateProvider.notifier);
 | 
				
			||||||
 | 
					      wsState.sendMessage(
 | 
				
			||||||
 | 
					        jsonEncode(
 | 
				
			||||||
 | 
					          WebSocketPacket(
 | 
				
			||||||
 | 
					            type: 'messages.subscribe',
 | 
				
			||||||
 | 
					            data: {'chat_room_id': id},
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return () {
 | 
				
			||||||
 | 
					        wsState.sendMessage(
 | 
				
			||||||
 | 
					          jsonEncode(
 | 
				
			||||||
 | 
					            WebSocketPacket(
 | 
				
			||||||
 | 
					              type: 'messages.unsubscribe',
 | 
				
			||||||
 | 
					              data: {'chat_room_id': id},
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }, [id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Future<void> pickPhotoMedia() async {
 | 
					    Future<void> pickPhotoMedia() async {
 | 
				
			||||||
      final result = await ref
 | 
					      final result = await ref
 | 
				
			||||||
          .watch(imagePickerProvider)
 | 
					          .watch(imagePickerProvider)
 | 
				
			||||||
@@ -617,7 +687,7 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
          IconButton(
 | 
					          IconButton(
 | 
				
			||||||
            icon: const Icon(Icons.more_vert),
 | 
					            icon: const Icon(Icons.more_vert),
 | 
				
			||||||
            onPressed: () {
 | 
					            onPressed: () {
 | 
				
			||||||
              context.push('/chat/id/detail');
 | 
					              context.push('/chat/$id/detail');
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          const Gap(8),
 | 
					          const Gap(8),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +1,64 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/widgets/realm/realm_list.dart';
 | 
					import 'package:island/widgets/realm/realm_list.dart';
 | 
				
			||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DiscoveryRealmsScreen extends HookConsumerWidget {
 | 
					class DiscoveryRealmsScreen extends HookConsumerWidget {
 | 
				
			||||||
  const DiscoveryRealmsScreen({super.key});
 | 
					  const DiscoveryRealmsScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    Timer? debounceTimer;
 | 
				
			||||||
 | 
					    final searchController = useTextEditingController();
 | 
				
			||||||
 | 
					    final currentQuery = useState<String?>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(title: Text('discoverRealms'.tr())),
 | 
					      appBar: AppBar(title: Text('discoverRealms'.tr())),
 | 
				
			||||||
      body: CustomScrollView(
 | 
					      body: Stack(
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          CustomScrollView(
 | 
				
			||||||
            slivers: [
 | 
					            slivers: [
 | 
				
			||||||
          SliverGap(16),
 | 
					              SliverGap(80),
 | 
				
			||||||
          SliverRealmList(),
 | 
					              SliverRealmList(
 | 
				
			||||||
 | 
					                query: currentQuery.value,
 | 
				
			||||||
 | 
					                key: ValueKey(currentQuery.value),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
              SliverGap(MediaQuery.of(context).padding.bottom + 16),
 | 
					              SliverGap(MediaQuery.of(context).padding.bottom + 16),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					          Positioned(
 | 
				
			||||||
 | 
					            top: 0,
 | 
				
			||||||
 | 
					            left: 0,
 | 
				
			||||||
 | 
					            right: 0,
 | 
				
			||||||
 | 
					            child: Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.all(16),
 | 
				
			||||||
 | 
					              child: SearchBar(
 | 
				
			||||||
 | 
					                elevation: WidgetStateProperty.all(4),
 | 
				
			||||||
 | 
					                controller: searchController,
 | 
				
			||||||
 | 
					                hintText: 'search'.tr(),
 | 
				
			||||||
 | 
					                leading: const Icon(Icons.search),
 | 
				
			||||||
 | 
					                padding: WidgetStateProperty.all(
 | 
				
			||||||
 | 
					                  const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                onChanged: (value) {
 | 
				
			||||||
 | 
					                  if (debounceTimer?.isActive ?? false) {
 | 
				
			||||||
 | 
					                    debounceTimer?.cancel();
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                  debounceTimer = Timer(const Duration(milliseconds: 300), () {
 | 
				
			||||||
 | 
					                    if (currentQuery.value != value) {
 | 
				
			||||||
 | 
					                      currentQuery.value = value;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
@@ -12,13 +13,13 @@ import 'package:island/widgets/app_scaffold.dart';
 | 
				
			|||||||
import 'package:island/models/post.dart';
 | 
					import 'package:island/models/post.dart';
 | 
				
			||||||
import 'package:island/widgets/check_in.dart';
 | 
					import 'package:island/widgets/check_in.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_item.dart';
 | 
					import 'package:island/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:island/widgets/tour/tour.dart';
 | 
					 | 
				
			||||||
import 'package:island/screens/tabs.dart';
 | 
					import 'package:island/screens/tabs.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
					import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/widgets/realm/realm_card.dart';
 | 
					import 'package:island/widgets/realm/realm_card.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/publisher/publisher_card.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'explore.g.dart';
 | 
					part 'explore.g.dart';
 | 
				
			||||||
@@ -85,8 +86,7 @@ class ExploreScreen extends HookConsumerWidget {
 | 
				
			|||||||
      activityListNotifierProvider(currentFilter.value).notifier,
 | 
					      activityListNotifierProvider(currentFilter.value).notifier,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return TourTriggerWidget(
 | 
					    return AppScaffold(
 | 
				
			||||||
      child: AppScaffold(
 | 
					 | 
				
			||||||
      extendBody: false, // Prevent conflicts with tabs navigation
 | 
					      extendBody: false, // Prevent conflicts with tabs navigation
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        toolbarHeight: 0,
 | 
					        toolbarHeight: 0,
 | 
				
			||||||
@@ -137,13 +137,13 @@ class ExploreScreen extends HookConsumerWidget {
 | 
				
			|||||||
      floatingActionButtonLocation: TabbedFabLocation(context),
 | 
					      floatingActionButtonLocation: TabbedFabLocation(context),
 | 
				
			||||||
      body: TabBarView(
 | 
					      body: TabBarView(
 | 
				
			||||||
        controller: tabController,
 | 
					        controller: tabController,
 | 
				
			||||||
 | 
					        physics: const NeverScrollableScrollPhysics(),
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          _buildActivityList(ref, null),
 | 
					          _buildActivityList(ref, null),
 | 
				
			||||||
          _buildActivityList(ref, 'subscriptions'),
 | 
					          _buildActivityList(ref, 'subscriptions'),
 | 
				
			||||||
          _buildActivityList(ref, 'friends'),
 | 
					          _buildActivityList(ref, 'friends'),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -180,10 +180,8 @@ class _DiscoveryActivityItem extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final items =
 | 
					    final items = data['items'] as List;
 | 
				
			||||||
        (data['items'] as List)
 | 
					    final type = items.firstOrNull?['type'] ?? 'unknown';
 | 
				
			||||||
            .map((e) => SnRealm.fromJson(e['data'] as Map<String, dynamic>))
 | 
					 | 
				
			||||||
            .toList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Column(
 | 
					    return Column(
 | 
				
			||||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
					      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
@@ -194,7 +192,11 @@ class _DiscoveryActivityItem extends StatelessWidget {
 | 
				
			|||||||
            const Icon(Symbols.explore, size: 19),
 | 
					            const Icon(Symbols.explore, size: 19),
 | 
				
			||||||
            const Gap(8),
 | 
					            const Gap(8),
 | 
				
			||||||
            Text(
 | 
					            Text(
 | 
				
			||||||
              'discoverCommunities'.tr(),
 | 
					              (switch (type) {
 | 
				
			||||||
 | 
					                'realm' => 'discoverRealms',
 | 
				
			||||||
 | 
					                'publisher' => 'discoverPublishers',
 | 
				
			||||||
 | 
					                _ => 'unknown',
 | 
				
			||||||
 | 
					              }).tr(),
 | 
				
			||||||
              style: Theme.of(context).textTheme.titleMedium,
 | 
					              style: Theme.of(context).textTheme.titleMedium,
 | 
				
			||||||
            ).padding(top: 1),
 | 
					            ).padding(top: 1),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
@@ -204,13 +206,26 @@ class _DiscoveryActivityItem extends StatelessWidget {
 | 
				
			|||||||
          child: ListView.builder(
 | 
					          child: ListView.builder(
 | 
				
			||||||
            scrollDirection: Axis.horizontal,
 | 
					            scrollDirection: Axis.horizontal,
 | 
				
			||||||
            itemCount: items.length,
 | 
					            itemCount: items.length,
 | 
				
			||||||
            padding: const EdgeInsets.only(right: 8),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 8),
 | 
				
			||||||
            itemBuilder: (context, index) {
 | 
					            itemBuilder: (context, index) {
 | 
				
			||||||
              final realm = items[index];
 | 
					              final item = items[index];
 | 
				
			||||||
              return RealmCard(realm: realm);
 | 
					              switch (type) {
 | 
				
			||||||
 | 
					                case 'realm':
 | 
				
			||||||
 | 
					                  return RealmCard(
 | 
				
			||||||
 | 
					                    realm: SnRealm.fromJson(item['data']),
 | 
				
			||||||
 | 
					                    maxWidth: 280,
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                case 'publisher':
 | 
				
			||||||
 | 
					                  return PublisherCard(
 | 
				
			||||||
 | 
					                    publisher: SnPublisher.fromJson(item['data']),
 | 
				
			||||||
 | 
					                    maxWidth: 280,
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                  return Placeholder();
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ).padding(bottom: 4),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -326,6 +341,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
 | 
				
			|||||||
      if (cursor != null) 'cursor': cursor,
 | 
					      if (cursor != null) 'cursor': cursor,
 | 
				
			||||||
      'take': take,
 | 
					      'take': take,
 | 
				
			||||||
      if (filter != null) 'filter': filter,
 | 
					      if (filter != null) 'filter': filter,
 | 
				
			||||||
 | 
					      if (kDebugMode) 'debugInclude': 'realms,publishers',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final response = await client.get(
 | 
					    final response = await client.get(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ part of 'explore.dart';
 | 
				
			|||||||
// **************************************************************************
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$activityListNotifierHash() =>
 | 
					String _$activityListNotifierHash() =>
 | 
				
			||||||
    r'14ec2f211c86e1e64a9a34b142d0e8f78ff6361a';
 | 
					    r'57e9dcec944a9f88f8508b69fc91342592f5b349';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Copied from Dart SDK
 | 
					/// Copied from Dart SDK
 | 
				
			||||||
class _SystemHash {
 | 
					class _SystemHash {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,19 +4,18 @@ import 'dart:math' as math;
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/user.dart';
 | 
					import 'package:island/models/user.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/pods/websocket.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/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/widgets/content/markdown.dart';
 | 
					import 'package:island/widgets/content/markdown.dart';
 | 
				
			||||||
import 'package:relative_time/relative_time.dart';
 | 
					import 'package:relative_time/relative_time.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
					import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.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';
 | 
					part 'notification.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -180,36 +179,17 @@ class NotificationScreen extends HookConsumerWidget {
 | 
				
			|||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                  onTap: () {
 | 
					                  onTap: () {
 | 
				
			||||||
                    if (notification.meta['link'] is String) {
 | 
					                    if (notification.meta['action_uri'] != null) {
 | 
				
			||||||
                      final href = notification.meta['link'];
 | 
					                      var uri = notification.meta['action_uri'] as String;
 | 
				
			||||||
                      final uri = Uri.tryParse(href);
 | 
					                      if (uri.startsWith('/')) {
 | 
				
			||||||
                      if (uri == null) {
 | 
					                        // In-app routes
 | 
				
			||||||
                        showSnackBar(
 | 
					                        rootNavigatorKey.currentContext?.push(
 | 
				
			||||||
                          'brokenLink'.tr(args: []),
 | 
					                          notification.meta['action_uri'],
 | 
				
			||||||
                          action: SnackBarAction(
 | 
					 | 
				
			||||||
                            label: 'copyToClipboard'.tr(),
 | 
					 | 
				
			||||||
                            onPressed: () {
 | 
					 | 
				
			||||||
                              Clipboard.setData(ClipboardData(text: href));
 | 
					 | 
				
			||||||
                              clearSnackBar(context);
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        );
 | 
					                        );
 | 
				
			||||||
                        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);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,6 +54,7 @@ Future<SnSubscriptionStatus> publisherSubscriptionStatus(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
 | 
					Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    final publisher = await ref.watch(publisherProvider(pubName).future);
 | 
					    final publisher = await ref.watch(publisherProvider(pubName).future);
 | 
				
			||||||
    if (publisher.background == null) return null;
 | 
					    if (publisher.background == null) return null;
 | 
				
			||||||
    final palette = await PaletteGenerator.fromImageProvider(
 | 
					    final palette = await PaletteGenerator.fromImageProvider(
 | 
				
			||||||
@@ -65,14 +66,14 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
 | 
				
			|||||||
    final dominantColor = palette.dominantColor?.color;
 | 
					    final dominantColor = palette.dominantColor?.color;
 | 
				
			||||||
    if (dominantColor == null) return null;
 | 
					    if (dominantColor == null) return null;
 | 
				
			||||||
    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
					    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
				
			||||||
 | 
					  } catch (_) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PublisherProfileScreen extends HookConsumerWidget {
 | 
					class PublisherProfileScreen extends HookConsumerWidget {
 | 
				
			||||||
  final String name;
 | 
					  final String name;
 | 
				
			||||||
  const PublisherProfileScreen({
 | 
					  const PublisherProfileScreen({super.key, required this.name});
 | 
				
			||||||
    super.key,
 | 
					 | 
				
			||||||
    required this.name,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -155,7 +155,7 @@ class RealmDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                    ),
 | 
					                                    ),
 | 
				
			||||||
                                  ],
 | 
					                                  ],
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                                if (identity == null && realm.isPublic)
 | 
					                                if (identity == null && realm.isCommunity)
 | 
				
			||||||
                                  FilledButton.tonalIcon(
 | 
					                                  FilledButton.tonalIcon(
 | 
				
			||||||
                                    onPressed: () async {
 | 
					                                    onPressed: () async {
 | 
				
			||||||
                                      try {
 | 
					                                      try {
 | 
				
			||||||
@@ -169,14 +169,14 @@ class RealmDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                          realmIdentityProvider(slug),
 | 
					                                          realmIdentityProvider(slug),
 | 
				
			||||||
                                        );
 | 
					                                        );
 | 
				
			||||||
                                        ref.invalidate(realmsJoinedProvider);
 | 
					                                        ref.invalidate(realmsJoinedProvider);
 | 
				
			||||||
                                        showSnackBar('joinRealmSuccess'.tr());
 | 
					                                        showSnackBar('realmJoinSuccess'.tr());
 | 
				
			||||||
                                      } catch (err) {
 | 
					                                      } catch (err) {
 | 
				
			||||||
                                        showErrorAlert(err);
 | 
					                                        showErrorAlert(err);
 | 
				
			||||||
                                      }
 | 
					                                      }
 | 
				
			||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                                    icon: const Icon(Symbols.add),
 | 
					                                    icon: const Icon(Symbols.add),
 | 
				
			||||||
                                    label: const Text('joinRealm').tr(),
 | 
					                                    label: const Text('realmJoin').tr(),
 | 
				
			||||||
                                  ).padding(horizontal: 16, vertical: 8)
 | 
					                                  ).padding(horizontal: 16, vertical: 16)
 | 
				
			||||||
                                else
 | 
					                                else
 | 
				
			||||||
                                  const SizedBox.shrink(),
 | 
					                                  const SizedBox.shrink(),
 | 
				
			||||||
                              ],
 | 
					                              ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,9 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			|||||||
            var uri = notification.meta['action_uri'] as String;
 | 
					            var uri = notification.meta['action_uri'] as String;
 | 
				
			||||||
            if (uri.startsWith('/')) {
 | 
					            if (uri.startsWith('/')) {
 | 
				
			||||||
              // In-app routes
 | 
					              // In-app routes
 | 
				
			||||||
              rootNavigatorKey.currentContext?.push(notification.meta['action_uri']);
 | 
					              rootNavigatorKey.currentContext?.push(
 | 
				
			||||||
 | 
					                notification.meta['action_uri'],
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
              // External URLs
 | 
					              // External URLs
 | 
				
			||||||
              launchUrlString(uri);
 | 
					              launchUrlString(uri);
 | 
				
			||||||
@@ -46,8 +48,14 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			|||||||
        padding: EdgeInsets.only(
 | 
					        padding: EdgeInsets.only(
 | 
				
			||||||
          left: 16,
 | 
					          left: 16,
 | 
				
			||||||
          right: 16,
 | 
					          right: 16,
 | 
				
			||||||
 | 
					          top:
 | 
				
			||||||
 | 
					              (!kIsWeb &&
 | 
				
			||||||
 | 
					                      (Platform.isMacOS ||
 | 
				
			||||||
 | 
					                          Platform.isWindows ||
 | 
				
			||||||
 | 
					                          Platform.isLinux))
 | 
				
			||||||
 | 
					                  ? 24
 | 
				
			||||||
                  // ignore: use_build_context_synchronously
 | 
					                  // ignore: use_build_context_synchronously
 | 
				
			||||||
          top: MediaQuery.of(context).padding.top + 24,
 | 
					                  : MediaQuery.of(context).padding.top + 8,
 | 
				
			||||||
          bottom: 16,
 | 
					          bottom: 16,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/services/notify.dart';
 | 
					import 'package:island/services/notify.dart';
 | 
				
			||||||
import 'package:island/services/sharing_intent.dart';
 | 
					import 'package:island/services/sharing_intent.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/tour/tour.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppWrapper extends HookConsumerWidget {
 | 
					class AppWrapper extends HookConsumerWidget {
 | 
				
			||||||
  final Widget child;
 | 
					  final Widget child;
 | 
				
			||||||
@@ -24,6 +25,6 @@ class AppWrapper extends HookConsumerWidget {
 | 
				
			|||||||
      };
 | 
					      };
 | 
				
			||||||
    }, const []);
 | 
					    }, const []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return child;
 | 
					    return TourTriggerWidget(child: child);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -186,13 +186,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
 | 
				
			|||||||
    Future<void> saveToGallery() async {
 | 
					    Future<void> saveToGallery() async {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        // Show loading indicator
 | 
					        // Show loading indicator
 | 
				
			||||||
        final scaffold = ScaffoldMessenger.of(context);
 | 
					        showSnackBar('Saving image to gallery...');
 | 
				
			||||||
        scaffold.showSnackBar(
 | 
					 | 
				
			||||||
          const SnackBar(
 | 
					 | 
				
			||||||
            content: Text('Saving image to gallery...'),
 | 
					 | 
				
			||||||
            duration: Duration(seconds: 1),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Get the image URL
 | 
					        // Get the image URL
 | 
				
			||||||
        final client = ref.watch(apiClientProvider);
 | 
					        final client = ref.watch(apiClientProvider);
 | 
				
			||||||
@@ -209,12 +203,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
 | 
				
			|||||||
        await Gal.putImage(filePath, album: 'Solar Network');
 | 
					        await Gal.putImage(filePath, album: 'Solar Network');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Show success message
 | 
					        // Show success message
 | 
				
			||||||
        scaffold.showSnackBar(
 | 
					        showSnackBar('Image saved to gallery');
 | 
				
			||||||
          const SnackBar(
 | 
					 | 
				
			||||||
            content: Text('Image saved to gallery'),
 | 
					 | 
				
			||||||
            duration: Duration(seconds: 2),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        showErrorAlert(e);
 | 
					        showErrorAlert(e);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										100
									
								
								lib/widgets/publisher/publisher_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								lib/widgets/publisher/publisher_card.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/post.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PublisherCard extends ConsumerWidget {
 | 
				
			||||||
 | 
					  final SnPublisher publisher;
 | 
				
			||||||
 | 
					  final double? maxWidth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const PublisherCard({super.key, required this.publisher, this.maxWidth});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    Widget imageWidget;
 | 
				
			||||||
 | 
					    if (publisher.picture != null) {
 | 
				
			||||||
 | 
					      imageWidget = CloudImageWidget(
 | 
				
			||||||
 | 
					        file: publisher.background,
 | 
				
			||||||
 | 
					        fit: BoxFit.cover,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      imageWidget = ColoredBox(
 | 
				
			||||||
 | 
					        color: Theme.of(context).colorScheme.secondaryContainer,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget card = Card(
 | 
				
			||||||
 | 
					      clipBehavior: Clip.antiAlias,
 | 
				
			||||||
 | 
					      child: InkWell(
 | 
				
			||||||
 | 
					        onTap: () {
 | 
				
			||||||
 | 
					          context.push('/publishers/${publisher.name}');
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        child: AspectRatio(
 | 
				
			||||||
 | 
					          aspectRatio: 16 / 7,
 | 
				
			||||||
 | 
					          child: Stack(
 | 
				
			||||||
 | 
					            fit: StackFit.expand,
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              imageWidget,
 | 
				
			||||||
 | 
					              Positioned(
 | 
				
			||||||
 | 
					                bottom: 0,
 | 
				
			||||||
 | 
					                left: 0,
 | 
				
			||||||
 | 
					                right: 0,
 | 
				
			||||||
 | 
					                child: Container(
 | 
				
			||||||
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                    gradient: LinearGradient(
 | 
				
			||||||
 | 
					                      begin: Alignment.bottomCenter,
 | 
				
			||||||
 | 
					                      end: Alignment.topCenter,
 | 
				
			||||||
 | 
					                      colors: [
 | 
				
			||||||
 | 
					                        Colors.black.withOpacity(0.7),
 | 
				
			||||||
 | 
					                        Colors.transparent,
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  padding: const EdgeInsets.all(8),
 | 
				
			||||||
 | 
					                  child: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Container(
 | 
				
			||||||
 | 
					                        decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                          shape: BoxShape.circle,
 | 
				
			||||||
 | 
					                          boxShadow: [
 | 
				
			||||||
 | 
					                            BoxShadow(
 | 
				
			||||||
 | 
					                              color: Colors.black.withOpacity(0.5),
 | 
				
			||||||
 | 
					                              blurRadius: 4,
 | 
				
			||||||
 | 
					                              offset: const Offset(0, 2),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        child: ProfilePictureWidget(
 | 
				
			||||||
 | 
					                          file: publisher.picture,
 | 
				
			||||||
 | 
					                          radius: 12,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      const Gap(2),
 | 
				
			||||||
 | 
					                      Text(
 | 
				
			||||||
 | 
					                        publisher.nick,
 | 
				
			||||||
 | 
					                        style: Theme.of(context).textTheme.titleSmall?.copyWith(
 | 
				
			||||||
 | 
					                          color: Colors.white,
 | 
				
			||||||
 | 
					                          fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        maxLines: 2,
 | 
				
			||||||
 | 
					                        overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ConstrainedBox(
 | 
				
			||||||
 | 
					      constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
 | 
				
			||||||
 | 
					      child: card,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,41 +1,33 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/realm.dart';
 | 
					import 'package:island/models/realm.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RealmCard extends ConsumerWidget {
 | 
					class RealmCard extends ConsumerWidget {
 | 
				
			||||||
  final SnRealm realm;
 | 
					  final SnRealm realm;
 | 
				
			||||||
 | 
					  final double? maxWidth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const RealmCard({super.key, required this.realm});
 | 
					  const RealmCard({super.key, required this.realm, this.maxWidth});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final client = ref.watch(apiClientProvider);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Widget imageWidget;
 | 
					    Widget imageWidget;
 | 
				
			||||||
    if (realm.picture != null) {
 | 
					    if (realm.picture != null) {
 | 
				
			||||||
      final imageUrl = '${client.options.baseUrl}/files/${realm.picture!.id}';
 | 
					      imageWidget =
 | 
				
			||||||
      imageWidget = Image.network(
 | 
					          imageWidget = CloudImageWidget(
 | 
				
			||||||
        imageUrl,
 | 
					            file: realm.background,
 | 
				
			||||||
            fit: BoxFit.cover,
 | 
					            fit: BoxFit.cover,
 | 
				
			||||||
        width: double.infinity,
 | 
					 | 
				
			||||||
        height: double.infinity,
 | 
					 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      imageWidget = Container(
 | 
					      imageWidget = ColoredBox(
 | 
				
			||||||
        color: Theme.of(context).colorScheme.secondaryContainer,
 | 
					        color: Theme.of(context).colorScheme.secondaryContainer,
 | 
				
			||||||
        child: Center(
 | 
					 | 
				
			||||||
          child: Icon(
 | 
					 | 
				
			||||||
            Symbols.photo_camera,
 | 
					 | 
				
			||||||
            color: Theme.of(context).colorScheme.onSecondaryContainer,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Card(
 | 
					    Widget card = Card(
 | 
				
			||||||
      clipBehavior: Clip.antiAlias,
 | 
					      clipBehavior: Clip.antiAlias,
 | 
				
			||||||
      child: InkWell(
 | 
					      child: InkWell(
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
@@ -44,6 +36,7 @@ class RealmCard extends ConsumerWidget {
 | 
				
			|||||||
        child: AspectRatio(
 | 
					        child: AspectRatio(
 | 
				
			||||||
          aspectRatio: 16 / 7,
 | 
					          aspectRatio: 16 / 7,
 | 
				
			||||||
          child: Stack(
 | 
					          child: Stack(
 | 
				
			||||||
 | 
					            fit: StackFit.expand,
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              imageWidget,
 | 
					              imageWidget,
 | 
				
			||||||
              Positioned(
 | 
					              Positioned(
 | 
				
			||||||
@@ -62,7 +55,28 @@ class RealmCard extends ConsumerWidget {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  padding: const EdgeInsets.all(8),
 | 
					                  padding: const EdgeInsets.all(8),
 | 
				
			||||||
                  child: Text(
 | 
					                  child: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Container(
 | 
				
			||||||
 | 
					                        decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                          shape: BoxShape.circle,
 | 
				
			||||||
 | 
					                          boxShadow: [
 | 
				
			||||||
 | 
					                            BoxShadow(
 | 
				
			||||||
 | 
					                              color: Colors.black.withOpacity(0.5),
 | 
				
			||||||
 | 
					                              blurRadius: 4,
 | 
				
			||||||
 | 
					                              offset: const Offset(0, 2),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        child: ProfilePictureWidget(
 | 
				
			||||||
 | 
					                          file: realm.picture,
 | 
				
			||||||
 | 
					                          fallbackIcon: Symbols.group,
 | 
				
			||||||
 | 
					                          radius: 12,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      const Gap(2),
 | 
				
			||||||
 | 
					                      Text(
 | 
				
			||||||
                        realm.name,
 | 
					                        realm.name,
 | 
				
			||||||
                        style: Theme.of(context).textTheme.titleSmall?.copyWith(
 | 
					                        style: Theme.of(context).textTheme.titleSmall?.copyWith(
 | 
				
			||||||
                          color: Colors.white,
 | 
					                          color: Colors.white,
 | 
				
			||||||
@@ -71,6 +85,8 @@ class RealmCard extends ConsumerWidget {
 | 
				
			|||||||
                        maxLines: 2,
 | 
					                        maxLines: 2,
 | 
				
			||||||
                        overflow: TextOverflow.ellipsis,
 | 
					                        overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
@@ -78,5 +94,10 @@ class RealmCard extends ConsumerWidget {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ConstrainedBox(
 | 
				
			||||||
 | 
					      constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
 | 
				
			||||||
 | 
					      child: card,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/realm.dart';
 | 
					import 'package:island/models/realm.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
@@ -14,16 +15,23 @@ class RealmListNotifier extends _$RealmListNotifier
 | 
				
			|||||||
  static const int _pageSize = 20;
 | 
					  static const int _pageSize = 20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<CursorPagingData<SnRealm>> build() {
 | 
					  Future<CursorPagingData<SnRealm>> build(String? query) {
 | 
				
			||||||
    return fetch(cursor: null);
 | 
					    return fetch(cursor: null, query: query);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<CursorPagingData<SnRealm>> fetch({required String? cursor}) async {
 | 
					  Future<CursorPagingData<SnRealm>> fetch({
 | 
				
			||||||
 | 
					    required String? cursor,
 | 
				
			||||||
 | 
					    String? query,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
    final client = ref.read(apiClientProvider);
 | 
					    final client = ref.read(apiClientProvider);
 | 
				
			||||||
    final offset = cursor == null ? 0 : int.parse(cursor);
 | 
					    final offset = cursor == null ? 0 : int.parse(cursor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final queryParams = {'offset': offset, 'take': _pageSize};
 | 
					    final queryParams = {
 | 
				
			||||||
 | 
					      'offset': offset,
 | 
				
			||||||
 | 
					      'take': _pageSize,
 | 
				
			||||||
 | 
					      if (query != null && query.isNotEmpty) 'query': query,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final response = await client.get(
 | 
					    final response = await client.get(
 | 
				
			||||||
      '/discovery/realms',
 | 
					      '/discovery/realms',
 | 
				
			||||||
@@ -45,16 +53,18 @@ class RealmListNotifier extends _$RealmListNotifier
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SliverRealmList extends HookConsumerWidget {
 | 
					class SliverRealmList extends HookConsumerWidget {
 | 
				
			||||||
  const SliverRealmList({super.key});
 | 
					  const SliverRealmList({super.key, this.query});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String? query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    return PagingHelperSliverView(
 | 
					    return PagingHelperSliverView(
 | 
				
			||||||
      provider: realmListNotifierProvider,
 | 
					      provider: realmListNotifierProvider(query),
 | 
				
			||||||
      futureRefreshable: realmListNotifierProvider.future,
 | 
					      futureRefreshable: realmListNotifierProvider(query).future,
 | 
				
			||||||
      notifierRefreshable: realmListNotifierProvider.notifier,
 | 
					      notifierRefreshable: realmListNotifierProvider(query).notifier,
 | 
				
			||||||
      contentBuilder:
 | 
					      contentBuilder:
 | 
				
			||||||
          (data, widgetCount, endItemView) => SliverList.builder(
 | 
					          (data, widgetCount, endItemView) => SliverList.separated(
 | 
				
			||||||
            itemCount: widgetCount,
 | 
					            itemCount: widgetCount,
 | 
				
			||||||
            itemBuilder: (context, index) {
 | 
					            itemBuilder: (context, index) {
 | 
				
			||||||
              if (index == widgetCount - 1) {
 | 
					              if (index == widgetCount - 1) {
 | 
				
			||||||
@@ -71,6 +81,7 @@ class SliverRealmList extends HookConsumerWidget {
 | 
				
			|||||||
                child: RealmCard(realm: realm),
 | 
					                child: RealmCard(realm: realm),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            separatorBuilder: (_, _) => const Gap(8),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,25 +6,174 @@ part of 'realm_list.dart';
 | 
				
			|||||||
// RiverpodGenerator
 | 
					// RiverpodGenerator
 | 
				
			||||||
// **************************************************************************
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$realmListNotifierHash() => r'440eb8c61db2059699191b904b6518a0b01ccd25';
 | 
					String _$realmListNotifierHash() => r'02dee373a5609a5617b04ffec395d09dea7ae070';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Copied from Dart SDK
 | 
				
			||||||
 | 
					class _SystemHash {
 | 
				
			||||||
 | 
					  _SystemHash._();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static int combine(int hash, int value) {
 | 
				
			||||||
 | 
					    // ignore: parameter_assignments
 | 
				
			||||||
 | 
					    hash = 0x1fffffff & (hash + value);
 | 
				
			||||||
 | 
					    // ignore: parameter_assignments
 | 
				
			||||||
 | 
					    hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
 | 
				
			||||||
 | 
					    return hash ^ (hash >> 6);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static int finish(int hash) {
 | 
				
			||||||
 | 
					    // ignore: parameter_assignments
 | 
				
			||||||
 | 
					    hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
 | 
				
			||||||
 | 
					    // ignore: parameter_assignments
 | 
				
			||||||
 | 
					    hash = hash ^ (hash >> 11);
 | 
				
			||||||
 | 
					    return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					abstract class _$RealmListNotifier
 | 
				
			||||||
 | 
					    extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealm>> {
 | 
				
			||||||
 | 
					  late final String? query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  FutureOr<CursorPagingData<SnRealm>> build(String? query);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// See also [RealmListNotifier].
 | 
					/// See also [RealmListNotifier].
 | 
				
			||||||
@ProviderFor(RealmListNotifier)
 | 
					@ProviderFor(RealmListNotifier)
 | 
				
			||||||
final realmListNotifierProvider = AutoDisposeAsyncNotifierProvider<
 | 
					const realmListNotifierProvider = RealmListNotifierFamily();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// See also [RealmListNotifier].
 | 
				
			||||||
 | 
					class RealmListNotifierFamily
 | 
				
			||||||
 | 
					    extends Family<AsyncValue<CursorPagingData<SnRealm>>> {
 | 
				
			||||||
 | 
					  /// See also [RealmListNotifier].
 | 
				
			||||||
 | 
					  const RealmListNotifierFamily();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// See also [RealmListNotifier].
 | 
				
			||||||
 | 
					  RealmListNotifierProvider call(String? query) {
 | 
				
			||||||
 | 
					    return RealmListNotifierProvider(query);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  RealmListNotifierProvider getProviderOverride(
 | 
				
			||||||
 | 
					    covariant RealmListNotifierProvider provider,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return call(provider.query);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const Iterable<ProviderOrFamily>? _dependencies = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Iterable<ProviderOrFamily>? get dependencies => _dependencies;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
 | 
				
			||||||
 | 
					      _allTransitiveDependencies;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? get name => r'realmListNotifierProvider';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// See also [RealmListNotifier].
 | 
				
			||||||
 | 
					class RealmListNotifierProvider
 | 
				
			||||||
 | 
					    extends
 | 
				
			||||||
 | 
					        AutoDisposeAsyncNotifierProviderImpl<
 | 
				
			||||||
          RealmListNotifier,
 | 
					          RealmListNotifier,
 | 
				
			||||||
          CursorPagingData<SnRealm>
 | 
					          CursorPagingData<SnRealm>
 | 
				
			||||||
>.internal(
 | 
					        > {
 | 
				
			||||||
  RealmListNotifier.new,
 | 
					  /// See also [RealmListNotifier].
 | 
				
			||||||
 | 
					  RealmListNotifierProvider(String? query)
 | 
				
			||||||
 | 
					    : this._internal(
 | 
				
			||||||
 | 
					        () => RealmListNotifier()..query = query,
 | 
				
			||||||
 | 
					        from: realmListNotifierProvider,
 | 
				
			||||||
        name: r'realmListNotifierProvider',
 | 
					        name: r'realmListNotifierProvider',
 | 
				
			||||||
        debugGetCreateSourceHash:
 | 
					        debugGetCreateSourceHash:
 | 
				
			||||||
            const bool.fromEnvironment('dart.vm.product')
 | 
					            const bool.fromEnvironment('dart.vm.product')
 | 
				
			||||||
                ? null
 | 
					                ? null
 | 
				
			||||||
                : _$realmListNotifierHash,
 | 
					                : _$realmListNotifierHash,
 | 
				
			||||||
  dependencies: null,
 | 
					        dependencies: RealmListNotifierFamily._dependencies,
 | 
				
			||||||
  allTransitiveDependencies: null,
 | 
					        allTransitiveDependencies:
 | 
				
			||||||
 | 
					            RealmListNotifierFamily._allTransitiveDependencies,
 | 
				
			||||||
 | 
					        query: query,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef _$RealmListNotifier =
 | 
					  RealmListNotifierProvider._internal(
 | 
				
			||||||
    AutoDisposeAsyncNotifier<CursorPagingData<SnRealm>>;
 | 
					    super._createNotifier, {
 | 
				
			||||||
 | 
					    required super.name,
 | 
				
			||||||
 | 
					    required super.dependencies,
 | 
				
			||||||
 | 
					    required super.allTransitiveDependencies,
 | 
				
			||||||
 | 
					    required super.debugGetCreateSourceHash,
 | 
				
			||||||
 | 
					    required super.from,
 | 
				
			||||||
 | 
					    required this.query,
 | 
				
			||||||
 | 
					  }) : super.internal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String? query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  FutureOr<CursorPagingData<SnRealm>> runNotifierBuild(
 | 
				
			||||||
 | 
					    covariant RealmListNotifier notifier,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return notifier.build(query);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Override overrideWith(RealmListNotifier Function() create) {
 | 
				
			||||||
 | 
					    return ProviderOverride(
 | 
				
			||||||
 | 
					      origin: this,
 | 
				
			||||||
 | 
					      override: RealmListNotifierProvider._internal(
 | 
				
			||||||
 | 
					        () => create()..query = query,
 | 
				
			||||||
 | 
					        from: from,
 | 
				
			||||||
 | 
					        name: null,
 | 
				
			||||||
 | 
					        dependencies: null,
 | 
				
			||||||
 | 
					        allTransitiveDependencies: null,
 | 
				
			||||||
 | 
					        debugGetCreateSourceHash: null,
 | 
				
			||||||
 | 
					        query: query,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  AutoDisposeAsyncNotifierProviderElement<
 | 
				
			||||||
 | 
					    RealmListNotifier,
 | 
				
			||||||
 | 
					    CursorPagingData<SnRealm>
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					  createElement() {
 | 
				
			||||||
 | 
					    return _RealmListNotifierProviderElement(this);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    return other is RealmListNotifierProvider && other.query == query;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode {
 | 
				
			||||||
 | 
					    var hash = _SystemHash.combine(0, runtimeType.hashCode);
 | 
				
			||||||
 | 
					    hash = _SystemHash.combine(hash, query.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return _SystemHash.finish(hash);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Deprecated('Will be removed in 3.0. Use Ref instead')
 | 
				
			||||||
 | 
					// ignore: unused_element
 | 
				
			||||||
 | 
					mixin RealmListNotifierRef
 | 
				
			||||||
 | 
					    on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnRealm>> {
 | 
				
			||||||
 | 
					  /// The parameter `query` of this provider.
 | 
				
			||||||
 | 
					  String? get query;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _RealmListNotifierProviderElement
 | 
				
			||||||
 | 
					    extends
 | 
				
			||||||
 | 
					        AutoDisposeAsyncNotifierProviderElement<
 | 
				
			||||||
 | 
					          RealmListNotifier,
 | 
				
			||||||
 | 
					          CursorPagingData<SnRealm>
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					    with RealmListNotifierRef {
 | 
				
			||||||
 | 
					  _RealmListNotifierProviderElement(super.provider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? get query => (origin as RealmListNotifierProvider).query;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ignore_for_file: type=lint
 | 
					// ignore_for_file: type=lint
 | 
				
			||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
 | 
					// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ import 'package:island/pods/network.dart';
 | 
				
			|||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/pods/userinfo.dart';
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
import 'package:island/services/file.dart';
 | 
					import 'package:island/services/file.dart';
 | 
				
			||||||
 | 
					import 'package:mime/mime.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
import 'package:path/path.dart' as path;
 | 
					import 'package:path/path.dart' as path;
 | 
				
			||||||
@@ -149,9 +150,9 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
 | 
				
			|||||||
        case ShareContentType.file:
 | 
					        case ShareContentType.file:
 | 
				
			||||||
          if (widget.content.files != null) {
 | 
					          if (widget.content.files != null) {
 | 
				
			||||||
            // Convert XFiles to UniversalFiles
 | 
					            // Convert XFiles to UniversalFiles
 | 
				
			||||||
            for (final xFile in widget.content.files!) {
 | 
					            for (final file in widget.content.files!) {
 | 
				
			||||||
              final file = File(xFile.path);
 | 
					              var mimeType = file.mimeType;
 | 
				
			||||||
              final mimeType = xFile.mimeType;
 | 
					              mimeType ??= lookupMimeType(file.path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              UniversalFileType fileType;
 | 
					              UniversalFileType fileType;
 | 
				
			||||||
              if (mimeType?.startsWith('image/') == true) {
 | 
					              if (mimeType?.startsWith('image/') == true) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,6 +49,8 @@ PODS:
 | 
				
			|||||||
    - OrderedSet (~> 6.0.3)
 | 
					    - OrderedSet (~> 6.0.3)
 | 
				
			||||||
  - flutter_platform_alert (0.0.1):
 | 
					  - flutter_platform_alert (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
 | 
					  - flutter_secure_storage_macos (6.1.3):
 | 
				
			||||||
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - flutter_timezone (0.1.0):
 | 
					  - flutter_timezone (0.1.0):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
@@ -171,6 +173,7 @@ DEPENDENCIES:
 | 
				
			|||||||
  - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
 | 
					  - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
 | 
				
			||||||
  - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
 | 
					  - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
 | 
				
			||||||
  - flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
 | 
					  - flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
 | 
				
			||||||
 | 
					  - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
 | 
				
			||||||
  - flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
 | 
					  - flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
 | 
				
			||||||
  - flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
 | 
					  - flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
 | 
				
			||||||
  - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
 | 
					  - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
 | 
				
			||||||
@@ -232,6 +235,8 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
 | 
				
			||||||
  flutter_platform_alert:
 | 
					  flutter_platform_alert:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos
 | 
				
			||||||
 | 
					  flutter_secure_storage_macos:
 | 
				
			||||||
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
 | 
				
			||||||
  flutter_timezone:
 | 
					  flutter_timezone:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos
 | 
				
			||||||
  flutter_udid:
 | 
					  flutter_udid:
 | 
				
			||||||
@@ -295,6 +300,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
 | 
					  FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
 | 
				
			||||||
  flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
 | 
					  flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
 | 
				
			||||||
  flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
 | 
					  flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
 | 
				
			||||||
 | 
					  flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
 | 
				
			||||||
  flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
 | 
					  flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
 | 
				
			||||||
  flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
 | 
					  flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
 | 
				
			||||||
  flutter_webrtc: a7eeb54859e672228c28f4b48b1fb61561976ea3
 | 
					  flutter_webrtc: a7eeb54859e672228c28f4b48b1fb61561976ea3
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -1470,7 +1470,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.16.0"
 | 
					    version: "1.16.0"
 | 
				
			||||||
  mime:
 | 
					  mime:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: mime
 | 
					      name: mime
 | 
				
			||||||
      sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
 | 
					      sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
 | 
				
			||||||
@@ -1785,10 +1785,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: record_linux
 | 
					      name: record_linux
 | 
				
			||||||
      sha256: "29e7735b05c1944bb6c9b72a36c08d4a1b24117e712d6a9523c003bde12bf484"
 | 
					      sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.0"
 | 
					    version: "1.1.1"
 | 
				
			||||||
  record_macos:
 | 
					  record_macos:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2254,10 +2254,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: synchronized
 | 
					      name: synchronized
 | 
				
			||||||
      sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
 | 
					      sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.3.1"
 | 
					    version: "3.4.0"
 | 
				
			||||||
  table_calendar:
 | 
					  table_calendar:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					# 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
 | 
					# 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.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 3.0.0+107
 | 
					version: 3.0.0+109
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.7.2
 | 
					  sdk: ^3.7.2
 | 
				
			||||||
@@ -126,6 +126,7 @@ dependencies:
 | 
				
			|||||||
    git:
 | 
					    git:
 | 
				
			||||||
      url: https://github.com/lionelmennig/textfield_tags.git
 | 
					      url: https://github.com/lionelmennig/textfield_tags.git
 | 
				
			||||||
      ref: fixes/allow-controller-re-registration
 | 
					      ref: fixes/allow-controller-re-registration
 | 
				
			||||||
 | 
					  mime: ^2.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user