Compare commits
22 Commits
e84d8f9455
...
63253e9cae
Author | SHA1 | Date | |
---|---|---|---|
63253e9cae | |||
26dafb4e9e | |||
111c66aa6a | |||
5b03fc9b47 | |||
672ae736d3 | |||
e37db3d00c | |||
919ed6f0a1 | |||
79867e4eaa | |||
99600bafb0 | |||
59aad79aa0 | |||
6fc4672a6e | |||
478c639ae7 | |||
7f2c978736 | |||
f472239102 | |||
f2844122c0 | |||
7781b12dac | |||
1e795b8b10 | |||
7d0896d84f | |||
fc0aade0ae | |||
d969622675 | |||
23839b6ec6 | |||
0b120c1e9c |
2
.gitlint
2
.gitlint
@ -7,7 +7,7 @@ line-length=72
|
|||||||
[title-trailing-punctuation]
|
[title-trailing-punctuation]
|
||||||
[title-hard-tab]
|
[title-hard-tab]
|
||||||
[title-match-regex]
|
[title-match-regex]
|
||||||
regex=^((feat|fix|chore|refactor)\((service|ui|shared|all|tests|i18n|docs|flake)+(,(service|ui|shared|all|tests|i18n|docs|flake))*\)|release): [A-Z0-9].*$
|
regex=^((feat|fix|chore|refactor)\((service|ui|shared|all|tests|i18n|docs|flake|android|ios|linux|windows|macos)+(,(service|ui|shared|all|tests|i18n|docs|flake|android|ios|linux|windows|macos))*\)|release): [A-Z0-9].*$
|
||||||
|
|
||||||
|
|
||||||
[body-trailing-whitespace]
|
[body-trailing-whitespace]
|
||||||
|
@ -56,6 +56,12 @@ Before creating a pull request, please make sure you checked every item on the f
|
|||||||
If you think that your code is ready for a pull request, but you are not sure if it is ready, prefix the PR's title with "WIP: ", so that discussion
|
If you think that your code is ready for a pull request, but you are not sure if it is ready, prefix the PR's title with "WIP: ", so that discussion
|
||||||
can happen there. If you think your PR is ready for review, remove the "WIP: " prefix.
|
can happen there. If you think your PR is ready for review, remove the "WIP: " prefix.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
In case you modified the Android-native code, please also make sure that you checked every item on the following checklist:
|
||||||
|
|
||||||
|
- [ ] I checked that [ktlint](https://github.com/pinterest/ktlint) is not showing any linting issues (`ktlint android/app/src/main/kotlin/org/moxxy/moxxyv2/ '!android/app/src/main/kotlin/org/moxxy/moxxyv2/Api.kt'`)
|
||||||
|
|
||||||
### Tips
|
### Tips
|
||||||
#### `data_classes.yaml`
|
#### `data_classes.yaml`
|
||||||
|
|
||||||
|
@ -14,3 +14,4 @@ analyzer:
|
|||||||
- "**/*.freezed.dart"
|
- "**/*.freezed.dart"
|
||||||
- "**/*.moxxy.dart"
|
- "**/*.moxxy.dart"
|
||||||
- "lib/i18n/*.dart"
|
- "lib/i18n/*.dart"
|
||||||
|
- "pigeon/api.dart"
|
||||||
|
@ -1,33 +1,35 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.moxxy.moxxyv2">
|
package="org.moxxy.moxxyv2">
|
||||||
<application
|
|
||||||
android:label="Moxxy"
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="Moxxy">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme" />
|
android:resource="@style/NormalTheme" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Allow receiving share intents for all kinds of things -->
|
<!-- Allow receiving share intents for all kinds of things -->
|
||||||
<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" />
|
||||||
@ -47,20 +49,37 @@
|
|||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/share_targets" />
|
android:resource="@xml/share_targets" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="org.moxxy.moxxyv2.notifications.MoxxyFileProvider"
|
||||||
|
android:authorities="org.moxxy.moxxyv2.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<receiver android:name=".notifications.NotificationReceiver" />
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<queries>
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<intent>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<data android:scheme="https" />
|
|
||||||
</intent>
|
<queries>
|
||||||
</queries>
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
666
android/app/src/main/kotlin/org/moxxy/moxxyv2/Api.kt
Normal file
666
android/app/src/main/kotlin/org/moxxy/moxxyv2/Api.kt
Normal file
@ -0,0 +1,666 @@
|
|||||||
|
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
|
package org.moxxy.moxxyv2
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
private fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
if (exception is FlutterError) {
|
||||||
|
return listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||||
|
* @property code The error code.
|
||||||
|
* @property message The error message.
|
||||||
|
* @property details The error details. Must be a datatype supported by the api codec.
|
||||||
|
*/
|
||||||
|
class FlutterError (
|
||||||
|
val code: String,
|
||||||
|
override val message: String? = null,
|
||||||
|
val details: Any? = null
|
||||||
|
) : Throwable()
|
||||||
|
|
||||||
|
enum class NotificationIcon(val raw: Int) {
|
||||||
|
WARNING(0),
|
||||||
|
ERROR(1),
|
||||||
|
NONE(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofRaw(raw: Int): NotificationIcon? {
|
||||||
|
return values().firstOrNull { it.raw == raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NotificationEventType(val raw: Int) {
|
||||||
|
MARKASREAD(0),
|
||||||
|
REPLY(1),
|
||||||
|
OPEN(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofRaw(raw: Int): NotificationEventType? {
|
||||||
|
return values().firstOrNull { it.raw == raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NotificationChannelImportance(val raw: Int) {
|
||||||
|
MIN(0),
|
||||||
|
HIGH(1),
|
||||||
|
DEFAULT(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofRaw(raw: Int): NotificationChannelImportance? {
|
||||||
|
return values().firstOrNull { it.raw == raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class NotificationMessageContent (
|
||||||
|
/** The textual body of the message. */
|
||||||
|
val body: String? = null,
|
||||||
|
/** The path and mime type of the media to show. */
|
||||||
|
val mime: String? = null,
|
||||||
|
val path: String? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): NotificationMessageContent {
|
||||||
|
val body = list[0] as String?
|
||||||
|
val mime = list[1] as String?
|
||||||
|
val path = list[2] as String?
|
||||||
|
return NotificationMessageContent(body, mime, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
body,
|
||||||
|
mime,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class NotificationMessage (
|
||||||
|
/** The grouping key for the notification. */
|
||||||
|
val groupId: String? = null,
|
||||||
|
/** The sender of the message. */
|
||||||
|
val sender: String? = null,
|
||||||
|
/** The jid of the sender. */
|
||||||
|
val jid: String? = null,
|
||||||
|
/** The body of the message. */
|
||||||
|
val content: NotificationMessageContent,
|
||||||
|
/** Milliseconds since epoch. */
|
||||||
|
val timestamp: Long,
|
||||||
|
/** The path to the avatar to use */
|
||||||
|
val avatarPath: String? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): NotificationMessage {
|
||||||
|
val groupId = list[0] as String?
|
||||||
|
val sender = list[1] as String?
|
||||||
|
val jid = list[2] as String?
|
||||||
|
val content = NotificationMessageContent.fromList(list[3] as List<Any?>)
|
||||||
|
val timestamp = list[4].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
val avatarPath = list[5] as String?
|
||||||
|
return NotificationMessage(groupId, sender, jid, content, timestamp, avatarPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
groupId,
|
||||||
|
sender,
|
||||||
|
jid,
|
||||||
|
content.toList(),
|
||||||
|
timestamp,
|
||||||
|
avatarPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class MessagingNotification (
|
||||||
|
/** The title of the conversation. */
|
||||||
|
val title: String,
|
||||||
|
/** The id of the notification. */
|
||||||
|
val id: Long,
|
||||||
|
/** The id of the notification channel the notification should appear on. */
|
||||||
|
val channelId: String,
|
||||||
|
/** The JID of the chat in which the notifications happen. */
|
||||||
|
val jid: String,
|
||||||
|
/** Messages to show. */
|
||||||
|
val messages: List<NotificationMessage?>,
|
||||||
|
/** Flag indicating whether this notification is from a groupchat or not. */
|
||||||
|
val isGroupchat: Boolean,
|
||||||
|
/** The id for notification grouping. */
|
||||||
|
val groupId: String? = null,
|
||||||
|
/** Additional data to include. */
|
||||||
|
val extra: Map<String?, String?>? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): MessagingNotification {
|
||||||
|
val title = list[0] as String
|
||||||
|
val id = list[1].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
val channelId = list[2] as String
|
||||||
|
val jid = list[3] as String
|
||||||
|
val messages = list[4] as List<NotificationMessage?>
|
||||||
|
val isGroupchat = list[5] as Boolean
|
||||||
|
val groupId = list[6] as String?
|
||||||
|
val extra = list[7] as Map<String?, String?>?
|
||||||
|
return MessagingNotification(title, id, channelId, jid, messages, isGroupchat, groupId, extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
title,
|
||||||
|
id,
|
||||||
|
channelId,
|
||||||
|
jid,
|
||||||
|
messages,
|
||||||
|
isGroupchat,
|
||||||
|
groupId,
|
||||||
|
extra,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class RegularNotification (
|
||||||
|
/** The title of the notification. */
|
||||||
|
val title: String,
|
||||||
|
/** The body of the notification. */
|
||||||
|
val body: String,
|
||||||
|
/** The id of the channel to show the notification on. */
|
||||||
|
val channelId: String,
|
||||||
|
/** The id for notification grouping. */
|
||||||
|
val groupId: String? = null,
|
||||||
|
/** The id of the notification. */
|
||||||
|
val id: Long,
|
||||||
|
/** The icon to use. */
|
||||||
|
val icon: NotificationIcon
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): RegularNotification {
|
||||||
|
val title = list[0] as String
|
||||||
|
val body = list[1] as String
|
||||||
|
val channelId = list[2] as String
|
||||||
|
val groupId = list[3] as String?
|
||||||
|
val id = list[4].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
val icon = NotificationIcon.ofRaw(list[5] as Int)!!
|
||||||
|
return RegularNotification(title, body, channelId, groupId, id, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
channelId,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
icon.raw,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class NotificationEvent (
|
||||||
|
/** The notification id. */
|
||||||
|
val id: Long,
|
||||||
|
/** The JID the notification was for. */
|
||||||
|
val jid: String,
|
||||||
|
/** The type of event. */
|
||||||
|
val type: NotificationEventType,
|
||||||
|
/**
|
||||||
|
* An optional payload.
|
||||||
|
* - type == NotificationType.reply: The reply message text.
|
||||||
|
* Otherwise: undefined.
|
||||||
|
*/
|
||||||
|
val payload: String? = null,
|
||||||
|
/** Extra data. Only set when type == NotificationType.reply. */
|
||||||
|
val extra: Map<String?, String?>? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): NotificationEvent {
|
||||||
|
val id = list[0].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
val jid = list[1] as String
|
||||||
|
val type = NotificationEventType.ofRaw(list[2] as Int)!!
|
||||||
|
val payload = list[3] as String?
|
||||||
|
val extra = list[4] as Map<String?, String?>?
|
||||||
|
return NotificationEvent(id, jid, type, payload, extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
id,
|
||||||
|
jid,
|
||||||
|
type.raw,
|
||||||
|
payload,
|
||||||
|
extra,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class NotificationI18nData (
|
||||||
|
/** The content of the reply button. */
|
||||||
|
val reply: String,
|
||||||
|
/** The content of the "mark as read" button. */
|
||||||
|
val markAsRead: String,
|
||||||
|
/** The text to show when *you* reply. */
|
||||||
|
val you: String
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): NotificationI18nData {
|
||||||
|
val reply = list[0] as String
|
||||||
|
val markAsRead = list[1] as String
|
||||||
|
val you = list[2] as String
|
||||||
|
return NotificationI18nData(reply, markAsRead, you)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
reply,
|
||||||
|
markAsRead,
|
||||||
|
you,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class NotificationGroup (
|
||||||
|
val id: String,
|
||||||
|
val description: String
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): NotificationGroup {
|
||||||
|
val id = list[0] as String
|
||||||
|
val description = list[1] as String
|
||||||
|
return NotificationGroup(id, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class NotificationChannel (
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val id: String,
|
||||||
|
val importance: NotificationChannelImportance,
|
||||||
|
val showBadge: Boolean,
|
||||||
|
val groupId: String? = null,
|
||||||
|
val vibration: Boolean,
|
||||||
|
val enableLights: Boolean
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): NotificationChannel {
|
||||||
|
val title = list[0] as String
|
||||||
|
val description = list[1] as String
|
||||||
|
val id = list[2] as String
|
||||||
|
val importance = NotificationChannelImportance.ofRaw(list[3] as Int)!!
|
||||||
|
val showBadge = list[4] as Boolean
|
||||||
|
val groupId = list[5] as String?
|
||||||
|
val vibration = list[6] as Boolean
|
||||||
|
val enableLights = list[7] as Boolean
|
||||||
|
return NotificationChannel(title, description, id, importance, showBadge, groupId, vibration, enableLights)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
id,
|
||||||
|
importance.raw,
|
||||||
|
showBadge,
|
||||||
|
groupId,
|
||||||
|
vibration,
|
||||||
|
enableLights,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private object MoxxyApiCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return when (type) {
|
||||||
|
128.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
MessagingNotification.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
129.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
NotificationChannel.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
130.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
NotificationEvent.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
131.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
NotificationGroup.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
132.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
NotificationI18nData.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
133.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
NotificationMessage.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
134.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
NotificationMessageContent.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
135.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
RegularNotification.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is MessagingNotification -> {
|
||||||
|
stream.write(128)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is NotificationChannel -> {
|
||||||
|
stream.write(129)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is NotificationEvent -> {
|
||||||
|
stream.write(130)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is NotificationGroup -> {
|
||||||
|
stream.write(131)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is NotificationI18nData -> {
|
||||||
|
stream.write(132)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is NotificationMessage -> {
|
||||||
|
stream.write(133)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is NotificationMessageContent -> {
|
||||||
|
stream.write(134)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is RegularNotification -> {
|
||||||
|
stream.write(135)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
else -> super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface MoxxyApi {
|
||||||
|
/** Notification APIs */
|
||||||
|
fun createNotificationGroups(groups: List<NotificationGroup>)
|
||||||
|
fun deleteNotificationGroups(ids: List<String>)
|
||||||
|
fun createNotificationChannels(channels: List<NotificationChannel>)
|
||||||
|
fun deleteNotificationChannels(ids: List<String>)
|
||||||
|
fun showMessagingNotification(notification: MessagingNotification)
|
||||||
|
fun showNotification(notification: RegularNotification)
|
||||||
|
fun dismissNotification(id: Long)
|
||||||
|
fun setNotificationSelfAvatar(path: String)
|
||||||
|
fun setNotificationI18n(data: NotificationI18nData)
|
||||||
|
fun notificationStub(event: NotificationEvent)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by MoxxyApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
MoxxyApiCodec
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `MoxxyApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyApi?) {
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.createNotificationGroups", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val groupsArg = args[0] as List<NotificationGroup>
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.createNotificationGroups(groupsArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.deleteNotificationGroups", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val idsArg = args[0] as List<String>
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.deleteNotificationGroups(idsArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.createNotificationChannels", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val channelsArg = args[0] as List<NotificationChannel>
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.createNotificationChannels(channelsArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.deleteNotificationChannels", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val idsArg = args[0] as List<String>
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.deleteNotificationChannels(idsArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.showMessagingNotification", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val notificationArg = args[0] as MessagingNotification
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.showMessagingNotification(notificationArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.showNotification", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val notificationArg = args[0] as RegularNotification
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.showNotification(notificationArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.dismissNotification", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val idArg = args[0].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.dismissNotification(idArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.setNotificationSelfAvatar", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val pathArg = args[0] as String
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.setNotificationSelfAvatar(pathArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.setNotificationI18n", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val dataArg = args[0] as NotificationI18nData
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.setNotificationI18n(dataArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyApi.notificationStub", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val eventArg = args[0] as NotificationEvent
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.notificationStub(eventArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
android/app/src/main/kotlin/org/moxxy/moxxyv2/Constants.kt
Normal file
32
android/app/src/main/kotlin/org/moxxy/moxxyv2/Constants.kt
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package org.moxxy.moxxyv2
|
||||||
|
|
||||||
|
// The tag we use for logging.
|
||||||
|
const val TAG = "Moxxy"
|
||||||
|
|
||||||
|
// The data key for text entered in the notification's reply field
|
||||||
|
const val REPLY_TEXT_KEY = "key_reply_text"
|
||||||
|
|
||||||
|
// The key for the notification id to mark as read
|
||||||
|
const val MARK_AS_READ_ID_KEY = "notification_id"
|
||||||
|
|
||||||
|
// Values for actions performed through the notification
|
||||||
|
const val REPLY_ACTION = "reply"
|
||||||
|
const val MARK_AS_READ_ACTION = "mark_as_read"
|
||||||
|
const val TAP_ACTION = "tap"
|
||||||
|
|
||||||
|
// Extra data keys for the intents that reach the NotificationReceiver
|
||||||
|
const val NOTIFICATION_EXTRA_JID_KEY = "jid"
|
||||||
|
const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"
|
||||||
|
|
||||||
|
// Extra data keys for messages embedded inside the notification style
|
||||||
|
const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime"
|
||||||
|
const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path"
|
||||||
|
|
||||||
|
const val MOXXY_FILEPROVIDER_ID = "org.moxxy.moxxyv2.fileprovider"
|
||||||
|
|
||||||
|
// Shared preferences keys
|
||||||
|
const val SHARED_PREFERENCES_KEY = "org.moxxy.moxxyv2"
|
||||||
|
const val SHARED_PREFERENCES_YOU_KEY = "you"
|
||||||
|
const val SHARED_PREFERENCES_MARK_AS_READ_KEY = "mark_as_read"
|
||||||
|
const val SHARED_PREFERENCES_REPLY_KEY = "reply"
|
||||||
|
const val SHARED_PREFERENCES_AVATAR_KEY = "avatar_path"
|
@ -1,6 +1,140 @@
|
|||||||
package org.moxxy.moxxyv2
|
package org.moxxy.moxxyv2
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
|
import org.moxxy.moxxyv2.notifications.NotificationDataManager
|
||||||
|
import org.moxxy.moxxyv2.notifications.createNotificationChannelsImpl
|
||||||
|
import org.moxxy.moxxyv2.notifications.createNotificationGroupsImpl
|
||||||
|
import org.moxxy.moxxyv2.notifications.extractPayloadMapFromIntent
|
||||||
|
import org.moxxy.moxxyv2.notifications.showNotificationImpl
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
object MoxxyEventChannels {
|
||||||
|
var notificationChannel: EventChannel? = null
|
||||||
|
var notificationEventSink: EventSink? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
object NotificationStreamHandler : EventChannel.StreamHandler {
|
||||||
|
override fun onListen(arguments: Any?, events: EventSink?) {
|
||||||
|
Log.d(TAG, "NotificationStreamHandler: Attached stream")
|
||||||
|
MoxxyEventChannels.notificationEventSink = events
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(arguments: Any?) {
|
||||||
|
Log.d(TAG, "NotificationStreamHandler: Detached stream")
|
||||||
|
MoxxyEventChannels.notificationEventSink = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hold the last notification event in case we did a cold start.
|
||||||
|
* TODO: Currently unused, but useful in the future.
|
||||||
|
* */
|
||||||
|
object NotificationCache {
|
||||||
|
var lastEvent: NotificationEvent? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity(), FlutterPlugin, MoxxyApi {
|
||||||
|
private var context: Context? = null
|
||||||
|
|
||||||
|
private fun handleIntent(intent: Intent?) {
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
when (intent.action) {
|
||||||
|
TAP_ACTION -> {
|
||||||
|
Log.d(TAG, "Handling tap data")
|
||||||
|
val event = NotificationEvent(
|
||||||
|
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
|
||||||
|
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
|
||||||
|
NotificationEventType.OPEN,
|
||||||
|
null,
|
||||||
|
extractPayloadMapFromIntent(intent),
|
||||||
|
)
|
||||||
|
NotificationCache.lastEvent = event
|
||||||
|
MoxxyEventChannels.notificationEventSink?.success(
|
||||||
|
event.toList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Log.d(TAG, "Unknown intent action: ${intent.action}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createNotificationGroups(groups: List<NotificationGroup>) {
|
||||||
|
createNotificationGroupsImpl(context!!, groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteNotificationGroups(ids: List<String>) {
|
||||||
|
val notificationManager = context!!.getSystemService(NotificationManager::class.java)
|
||||||
|
for (id in ids) {
|
||||||
|
notificationManager.deleteNotificationChannelGroup(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createNotificationChannels(channels: List<NotificationChannel>) {
|
||||||
|
createNotificationChannelsImpl(context!!, channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteNotificationChannels(ids: List<String>) {
|
||||||
|
val notificationManager = context!!.getSystemService(NotificationManager::class.java)
|
||||||
|
for (id in ids) {
|
||||||
|
notificationManager.deleteNotificationChannel(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showMessagingNotification(notification: MessagingNotification) {
|
||||||
|
org.moxxy.moxxyv2.notifications.showMessagingNotification(context!!, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showNotification(notification: RegularNotification) {
|
||||||
|
showNotificationImpl(context!!, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismissNotification(id: Long) {
|
||||||
|
NotificationManagerCompat.from(context!!).cancel(id.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setNotificationSelfAvatar(path: String) {
|
||||||
|
NotificationDataManager.setAvatarPath(context!!, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setNotificationI18n(data: NotificationI18nData) {
|
||||||
|
NotificationDataManager.apply {
|
||||||
|
setYou(context!!, data.you)
|
||||||
|
setReply(context!!, data.reply)
|
||||||
|
setMarkAsRead(context!!, data.markAsRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notificationStub(event: NotificationEvent) {}
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
context = binding.applicationContext
|
||||||
|
MoxxyEventChannels.notificationChannel = EventChannel(binding.binaryMessenger, "org.moxxy.moxxyv2/notification_stream")
|
||||||
|
MoxxyEventChannels.notificationChannel!!.setStreamHandler(NotificationStreamHandler)
|
||||||
|
|
||||||
|
MoxxyApi.setUp(binding.binaryMessenger, this)
|
||||||
|
|
||||||
|
Log.d(TAG, "Attached to engine")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
Log.d(TAG, "Detached from engine")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package org.moxxy.moxxyv2.notifications
|
||||||
|
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import org.moxxy.moxxyv2.R
|
||||||
|
|
||||||
|
class MoxxyFileProvider : FileProvider(R.xml.file_paths)
|
@ -0,0 +1,370 @@
|
|||||||
|
package org.moxxy.moxxyv2.notifications
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannelGroup
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.Person
|
||||||
|
import androidx.core.app.RemoteInput
|
||||||
|
import androidx.core.app.TaskStackBuilder
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import org.moxxy.moxxyv2.MARK_AS_READ_ACTION
|
||||||
|
import org.moxxy.moxxyv2.MOXXY_FILEPROVIDER_ID
|
||||||
|
import org.moxxy.moxxyv2.MainActivity
|
||||||
|
import org.moxxy.moxxyv2.MessagingNotification
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_EXTRA_ID_KEY
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_EXTRA_JID_KEY
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_MESSAGE_EXTRA_MIME
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_MESSAGE_EXTRA_PATH
|
||||||
|
import org.moxxy.moxxyv2.NotificationChannel
|
||||||
|
import org.moxxy.moxxyv2.NotificationChannelImportance
|
||||||
|
import org.moxxy.moxxyv2.NotificationGroup
|
||||||
|
import org.moxxy.moxxyv2.NotificationIcon
|
||||||
|
import org.moxxy.moxxyv2.R
|
||||||
|
import org.moxxy.moxxyv2.REPLY_ACTION
|
||||||
|
import org.moxxy.moxxyv2.REPLY_TEXT_KEY
|
||||||
|
import org.moxxy.moxxyv2.RegularNotification
|
||||||
|
import org.moxxy.moxxyv2.SHARED_PREFERENCES_AVATAR_KEY
|
||||||
|
import org.moxxy.moxxyv2.SHARED_PREFERENCES_KEY
|
||||||
|
import org.moxxy.moxxyv2.SHARED_PREFERENCES_MARK_AS_READ_KEY
|
||||||
|
import org.moxxy.moxxyv2.SHARED_PREFERENCES_REPLY_KEY
|
||||||
|
import org.moxxy.moxxyv2.SHARED_PREFERENCES_YOU_KEY
|
||||||
|
import org.moxxy.moxxyv2.TAG
|
||||||
|
import org.moxxy.moxxyv2.TAP_ACTION
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Holds "persistent" data for notifications, like i18n strings. While not useful now, this is
|
||||||
|
* useful for when the app is dead and we receive a notification.
|
||||||
|
* */
|
||||||
|
object NotificationDataManager {
|
||||||
|
private var you: String? = null
|
||||||
|
private var markAsRead: String? = null
|
||||||
|
private var reply: String? = null
|
||||||
|
|
||||||
|
private var fetchedAvatarPath = false
|
||||||
|
private var avatarPath: String? = null
|
||||||
|
|
||||||
|
private fun getString(context: Context, key: String, fallback: String): String {
|
||||||
|
return context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)!!.getString(key, fallback)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setString(context: Context, key: String, value: String) {
|
||||||
|
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit()
|
||||||
|
.putString(key, value)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getYou(context: Context): String {
|
||||||
|
if (you == null) you = getString(context, SHARED_PREFERENCES_YOU_KEY, "You")
|
||||||
|
return you!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setYou(context: Context, value: String) {
|
||||||
|
setString(context, SHARED_PREFERENCES_YOU_KEY, value)
|
||||||
|
you = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMarkAsRead(context: Context): String {
|
||||||
|
if (markAsRead == null) markAsRead = getString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, "Mark as read")
|
||||||
|
return markAsRead!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMarkAsRead(context: Context, value: String) {
|
||||||
|
setString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, value)
|
||||||
|
markAsRead = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getReply(context: Context): String {
|
||||||
|
if (reply != null) reply = getString(context, SHARED_PREFERENCES_REPLY_KEY, "Reply")
|
||||||
|
return reply!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReply(context: Context, value: String) {
|
||||||
|
setString(context, SHARED_PREFERENCES_REPLY_KEY, value)
|
||||||
|
reply = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAvatarPath(context: Context): String? {
|
||||||
|
if (avatarPath == null && !fetchedAvatarPath) {
|
||||||
|
val path = getString(context, SHARED_PREFERENCES_AVATAR_KEY, "")
|
||||||
|
if (path.isNotEmpty()) {
|
||||||
|
avatarPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatarPath
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAvatarPath(context: Context, value: String) {
|
||||||
|
setString(context, SHARED_PREFERENCES_AVATAR_KEY, value)
|
||||||
|
fetchedAvatarPath = true
|
||||||
|
avatarPath = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNotificationGroupsImpl(context: Context, groups: List<NotificationGroup>) {
|
||||||
|
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||||
|
for (group in groups) {
|
||||||
|
notificationManager.createNotificationChannelGroup(
|
||||||
|
NotificationChannelGroup(group.id, group.description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNotificationChannelsImpl(context: Context, channels: List<NotificationChannel>) {
|
||||||
|
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||||
|
for (channel in channels) {
|
||||||
|
val importance = when (channel.importance) {
|
||||||
|
NotificationChannelImportance.DEFAULT -> NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
NotificationChannelImportance.MIN -> NotificationManager.IMPORTANCE_MIN
|
||||||
|
NotificationChannelImportance.HIGH -> NotificationManager.IMPORTANCE_HIGH
|
||||||
|
}
|
||||||
|
val notificationChannel = android.app.NotificationChannel(channel.id, channel.title, importance).apply {
|
||||||
|
description = channel.description
|
||||||
|
|
||||||
|
enableVibration(channel.vibration)
|
||||||
|
enableLights(channel.enableLights)
|
||||||
|
setShowBadge(channel.showBadge)
|
||||||
|
|
||||||
|
if (channel.groupId != null) {
|
||||||
|
group = channel.groupId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notificationManager.createNotificationChannel(notificationChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// / Show a messaging style notification described by @notification.
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
fun showMessagingNotification(context: Context, notification: MessagingNotification) {
|
||||||
|
// Build the actions
|
||||||
|
// -> Reply action
|
||||||
|
val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply {
|
||||||
|
setLabel(NotificationDataManager.getReply(context))
|
||||||
|
}.build()
|
||||||
|
val replyIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = REPLY_ACTION
|
||||||
|
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
|
||||||
|
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
|
||||||
|
|
||||||
|
notification.extra?.forEach {
|
||||||
|
putExtra("payload_${it.key}", it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val replyPendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context.applicationContext,
|
||||||
|
0,
|
||||||
|
replyIntent,
|
||||||
|
PendingIntent.FLAG_MUTABLE,
|
||||||
|
)
|
||||||
|
val replyAction = NotificationCompat.Action.Builder(
|
||||||
|
R.drawable.reply,
|
||||||
|
NotificationDataManager.getReply(context),
|
||||||
|
replyPendingIntent,
|
||||||
|
).apply {
|
||||||
|
addRemoteInput(remoteInput)
|
||||||
|
setAllowGeneratedReplies(true)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
// -> Mark as read action
|
||||||
|
val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = MARK_AS_READ_ACTION
|
||||||
|
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
|
||||||
|
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
|
||||||
|
|
||||||
|
notification.extra?.forEach {
|
||||||
|
putExtra("payload_${it.key}", it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val markAsReadPendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context.applicationContext,
|
||||||
|
0,
|
||||||
|
markAsReadIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
val markAsReadAction = NotificationCompat.Action.Builder(
|
||||||
|
R.drawable.mark_as_read,
|
||||||
|
NotificationDataManager.getMarkAsRead(context),
|
||||||
|
markAsReadPendingIntent,
|
||||||
|
).build()
|
||||||
|
|
||||||
|
// -> Tap action
|
||||||
|
// NOTE: Because Android disallows "notification trampolines" (https://developer.android.com/about/versions/12/behavior-changes-12#notification-trampolines),
|
||||||
|
// we must do it this way instead of just using startActivity
|
||||||
|
val tapIntent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
action = TAP_ACTION
|
||||||
|
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
|
||||||
|
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
|
||||||
|
|
||||||
|
notification.extra?.forEach {
|
||||||
|
putExtra("payload_${it.key}", it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not launch a new task
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
val tapPendingIntent = TaskStackBuilder.create(context).run {
|
||||||
|
addNextIntentWithParentStack(tapIntent)
|
||||||
|
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
val selfPerson = Person.Builder().apply {
|
||||||
|
setName(NotificationDataManager.getYou(context))
|
||||||
|
|
||||||
|
// Set an avatar, if we have one
|
||||||
|
val avatarPath = NotificationDataManager.getAvatarPath(context)
|
||||||
|
if (avatarPath != null) {
|
||||||
|
setIcon(
|
||||||
|
IconCompat.createWithAdaptiveBitmap(
|
||||||
|
BitmapFactory.decodeFile(avatarPath),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
val style = NotificationCompat.MessagingStyle(selfPerson)
|
||||||
|
style.isGroupConversation = notification.isGroupchat
|
||||||
|
if (notification.isGroupchat) {
|
||||||
|
style.conversationTitle = notification.title
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in notification.messages.indices) {
|
||||||
|
val message = notification.messages[i]!!
|
||||||
|
|
||||||
|
// Build the sender
|
||||||
|
// NOTE: Note that we set it to null if message.sender == null because otherwise this results in
|
||||||
|
// a bogus Person object which messes with the "self-message" display as Android expects
|
||||||
|
// null in that case.
|
||||||
|
val sender = if (message.sender == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Person.Builder().apply {
|
||||||
|
setName(message.sender)
|
||||||
|
setKey(message.jid)
|
||||||
|
|
||||||
|
// Set the avatar, if available
|
||||||
|
if (message.avatarPath != null) {
|
||||||
|
try {
|
||||||
|
setIcon(
|
||||||
|
IconCompat.createWithAdaptiveBitmap(
|
||||||
|
BitmapFactory.decodeFile(message.avatarPath),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to open avatar at ${message.avatarPath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the message
|
||||||
|
val body = message.content.body ?: ""
|
||||||
|
val msg = NotificationCompat.MessagingStyle.Message(
|
||||||
|
body,
|
||||||
|
message.timestamp,
|
||||||
|
sender,
|
||||||
|
)
|
||||||
|
// If we got an image, turn it into a content URI and set it
|
||||||
|
if (message.content.mime != null && message.content.path != null) {
|
||||||
|
val fileUri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
MOXXY_FILEPROVIDER_ID,
|
||||||
|
File(message.content.path),
|
||||||
|
)
|
||||||
|
msg.apply {
|
||||||
|
setData(message.content.mime, fileUri)
|
||||||
|
|
||||||
|
extras.apply {
|
||||||
|
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, message.content.mime)
|
||||||
|
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, message.content.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the message
|
||||||
|
style.addMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the notification
|
||||||
|
val finalNotification = NotificationCompat.Builder(context, notification.channelId).apply {
|
||||||
|
setStyle(style)
|
||||||
|
// NOTE: It's okay to use the service icon here as I cannot get Android to display the
|
||||||
|
// actual logo. So we'll have to make do with the silhouette and the color purple.
|
||||||
|
setSmallIcon(R.drawable.ic_service_icon)
|
||||||
|
color = Color.argb(255, 207, 74, 255)
|
||||||
|
setColorized(true)
|
||||||
|
|
||||||
|
// Tap action
|
||||||
|
setContentIntent(tapPendingIntent)
|
||||||
|
|
||||||
|
// Notification actions
|
||||||
|
addAction(replyAction)
|
||||||
|
addAction(markAsReadAction)
|
||||||
|
|
||||||
|
// Groupchat title
|
||||||
|
if (notification.isGroupchat) {
|
||||||
|
setContentTitle(notification.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent grouping with the foreground service
|
||||||
|
if (notification.groupId != null) {
|
||||||
|
setGroup(notification.groupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAllowSystemGeneratedContextualActions(true)
|
||||||
|
setCategory(Notification.CATEGORY_MESSAGE)
|
||||||
|
|
||||||
|
// Prevent no notification when we replied before
|
||||||
|
setOnlyAlertOnce(false)
|
||||||
|
|
||||||
|
// Automatically dismiss the notification on tap
|
||||||
|
setAutoCancel(true)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
// Post the notification
|
||||||
|
try {
|
||||||
|
NotificationManagerCompat.from(context).notify(
|
||||||
|
notification.id.toInt(),
|
||||||
|
finalNotification,
|
||||||
|
)
|
||||||
|
} catch (ex: SecurityException) {
|
||||||
|
// Should never happen as Moxxy checks for the permission before posting the notification
|
||||||
|
Log.e(TAG, "Failed to post notification: ${ex.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showNotificationImpl(context: Context, notification: RegularNotification) {
|
||||||
|
val builtNotification = NotificationCompat.Builder(context, notification.channelId).apply {
|
||||||
|
setContentTitle(notification.title)
|
||||||
|
setContentText(notification.body)
|
||||||
|
|
||||||
|
when (notification.icon) {
|
||||||
|
NotificationIcon.ERROR -> setSmallIcon(R.drawable.error)
|
||||||
|
NotificationIcon.WARNING -> setSmallIcon(R.drawable.warning)
|
||||||
|
NotificationIcon.NONE -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.groupId != null) {
|
||||||
|
setGroup(notification.groupId)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
// Post the notification
|
||||||
|
try {
|
||||||
|
NotificationManagerCompat.from(context).notify(notification.id.toInt(), builtNotification)
|
||||||
|
} catch (ex: SecurityException) {
|
||||||
|
// Should never happen as Moxxy checks for the permission before posting the notification
|
||||||
|
Log.e(TAG, "Failed to post notification: ${ex.message}")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,214 @@
|
|||||||
|
package org.moxxy.moxxyv2.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.RemoteInput
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import org.moxxy.moxxyv2.MARK_AS_READ_ACTION
|
||||||
|
import org.moxxy.moxxyv2.MOXXY_FILEPROVIDER_ID
|
||||||
|
import org.moxxy.moxxyv2.MoxxyEventChannels
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_EXTRA_ID_KEY
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_EXTRA_JID_KEY
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_MESSAGE_EXTRA_MIME
|
||||||
|
import org.moxxy.moxxyv2.NOTIFICATION_MESSAGE_EXTRA_PATH
|
||||||
|
import org.moxxy.moxxyv2.NotificationEvent
|
||||||
|
import org.moxxy.moxxyv2.NotificationEventType
|
||||||
|
import org.moxxy.moxxyv2.REPLY_ACTION
|
||||||
|
import org.moxxy.moxxyv2.REPLY_TEXT_KEY
|
||||||
|
import org.moxxy.moxxyv2.TAG
|
||||||
|
import org.moxxy.moxxyv2.TAP_ACTION
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
|
||||||
|
val extras = mutableMapOf<String?, String?>()
|
||||||
|
intent.extras?.keySet()!!.forEach {
|
||||||
|
Log.d(TAG, "Checking $it -> ${intent.extras!!.get(it)}")
|
||||||
|
if (it.startsWith("payload_")) {
|
||||||
|
Log.d(TAG, "Adding $it")
|
||||||
|
extras[it.substring(8)] = intent.extras!!.getString(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extras
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationReceiver : BroadcastReceiver() {
|
||||||
|
/*
|
||||||
|
* Dismisses the notification through which we received @intent.
|
||||||
|
* */
|
||||||
|
private fun dismissNotification(context: Context, intent: Intent) {
|
||||||
|
// Dismiss the notification
|
||||||
|
val notificationId = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
|
||||||
|
if (notificationId != -1) {
|
||||||
|
NotificationManagerCompat.from(context).cancel(
|
||||||
|
notificationId,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Log.e("NotificationReceiver", "No id specified. Cannot dismiss notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findActiveNotification(context: Context, id: Int): Notification? {
|
||||||
|
return (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
||||||
|
.activeNotifications
|
||||||
|
.find { it.id == id }?.notification
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMarkAsRead(context: Context, intent: Intent) {
|
||||||
|
MoxxyEventChannels.notificationEventSink?.success(
|
||||||
|
NotificationEvent(
|
||||||
|
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
|
||||||
|
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
|
||||||
|
NotificationEventType.MARKASREAD,
|
||||||
|
null,
|
||||||
|
extractPayloadMapFromIntent(intent),
|
||||||
|
).toList(),
|
||||||
|
)
|
||||||
|
dismissNotification(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReply(context: Context, intent: Intent) {
|
||||||
|
val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
|
||||||
|
val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY)
|
||||||
|
MoxxyEventChannels.notificationEventSink?.success(
|
||||||
|
NotificationEvent(
|
||||||
|
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
|
||||||
|
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
|
||||||
|
NotificationEventType.REPLY,
|
||||||
|
replyPayload.toString(),
|
||||||
|
extractPayloadMapFromIntent(intent),
|
||||||
|
).toList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
|
||||||
|
if (id == -1) {
|
||||||
|
Log.e(TAG, "Failed to find notification id for reply")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = findActiveNotification(context, id)
|
||||||
|
if (notification == null) {
|
||||||
|
Log.e(TAG, "Failed to find notification for id $id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thanks https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c
|
||||||
|
val recoveredStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification)!!
|
||||||
|
val newStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
Notification.MessagingStyle(
|
||||||
|
android.app.Person.Builder().apply {
|
||||||
|
setName(NotificationDataManager.getYou(context))
|
||||||
|
|
||||||
|
// Set an avatar, if we have one
|
||||||
|
val avatarPath = NotificationDataManager.getAvatarPath(context)
|
||||||
|
if (avatarPath != null) {
|
||||||
|
setIcon(
|
||||||
|
Icon.createWithAdaptiveBitmap(
|
||||||
|
BitmapFactory.decodeFile(avatarPath),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.build(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Notification.MessagingStyle(NotificationDataManager.getYou(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
newStyle.apply {
|
||||||
|
conversationTitle = recoveredStyle.conversationTitle
|
||||||
|
recoveredStyle.messages.forEach {
|
||||||
|
// Check if we have to request (or refresh) the content URI to be able to still
|
||||||
|
// see the embedded image.
|
||||||
|
val mime = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_MIME)
|
||||||
|
val path = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_PATH)
|
||||||
|
val message = Notification.MessagingStyle.Message(it.text, it.timestamp, it.sender)
|
||||||
|
if (mime != null && path != null) {
|
||||||
|
// Request a new URI from the file provider to ensure we can still see the image
|
||||||
|
// in the notification
|
||||||
|
val fileUri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
MOXXY_FILEPROVIDER_ID,
|
||||||
|
File(path),
|
||||||
|
)
|
||||||
|
message.setData(
|
||||||
|
mime,
|
||||||
|
fileUri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// As we're creating a new message, also recreate the additional metadata
|
||||||
|
message.extras.apply {
|
||||||
|
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, mime)
|
||||||
|
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the old message
|
||||||
|
addMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append our new message
|
||||||
|
newStyle.addMessage(
|
||||||
|
Notification.MessagingStyle.Message(
|
||||||
|
replyPayload!!,
|
||||||
|
Instant.now().toEpochMilli(),
|
||||||
|
null as CharSequence?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Post the new notification
|
||||||
|
val recoveredBuilder = Notification.Builder.recoverBuilder(context, notification).apply {
|
||||||
|
style = newStyle
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
NotificationManagerCompat.from(context).notify(id, recoveredBuilder.build())
|
||||||
|
} catch (ex: SecurityException) {
|
||||||
|
Log.e(TAG, "Failed to post reply-notification: ${ex.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleTap(context: Context, intent: Intent) {
|
||||||
|
MoxxyEventChannels.notificationEventSink?.success(
|
||||||
|
NotificationEvent(
|
||||||
|
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
|
||||||
|
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
|
||||||
|
NotificationEventType.OPEN,
|
||||||
|
null,
|
||||||
|
extractPayloadMapFromIntent(intent),
|
||||||
|
).toList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bring the app into the foreground
|
||||||
|
Log.d(TAG, "Querying launch intent for ${context.packageName}")
|
||||||
|
val tapIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!
|
||||||
|
Log.d(TAG, "Starting activity")
|
||||||
|
context.startActivity(tapIntent)
|
||||||
|
|
||||||
|
// Dismiss the notification
|
||||||
|
Log.d(TAG, "Dismissing notification")
|
||||||
|
dismissNotification(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
// TODO: We need to be careful to ensure that the Flutter engine is running.
|
||||||
|
// If it's not, we have to start it. However, that's only an issue when we expect to
|
||||||
|
// receive notifications while not running, i.e. Push Notifications.
|
||||||
|
when (intent.action) {
|
||||||
|
MARK_AS_READ_ACTION -> handleMarkAsRead(context, intent)
|
||||||
|
REPLY_ACTION -> handleReply(context, intent)
|
||||||
|
TAP_ACTION -> handleTap(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
android/app/src/main/res/xml/file_paths.xml
Normal file
7
android/app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Media files -->
|
||||||
|
<files-path name="media" path="media/" />
|
||||||
|
|
||||||
|
<!-- Media thumbnails -->
|
||||||
|
<cache-path name="thumbnails" path="thumbnails/" />
|
||||||
|
</paths>
|
@ -24,7 +24,9 @@
|
|||||||
"messagesChannelName": "Messages",
|
"messagesChannelName": "Messages",
|
||||||
"messagesChannelDescription": "The notification channel for received messages",
|
"messagesChannelDescription": "The notification channel for received messages",
|
||||||
"warningChannelName": "Warnings",
|
"warningChannelName": "Warnings",
|
||||||
"warningChannelDescription": "Warnings related to Moxxy"
|
"warningChannelDescription": "Warnings related to Moxxy",
|
||||||
|
"serviceChannelName": "Foreground Service",
|
||||||
|
"serviceChannelDescription": "Holds the persistent foreground service notification"
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
"error": "Error"
|
"error": "Error"
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
- (Hopefully) fix OMEMO between two Moxxy clients.
|
- (Hopefully) fix OMEMO between two Moxxy clients.
|
||||||
- Allow correcting messages older than the last one. Whether all clients will accept such a correction is unclear.
|
- Allow correcting messages older than the last one. Whether all clients will accept such a correction is unclear.
|
||||||
- Add (incomplete) translations for Dutch, Japanese, and Russian.
|
- Add (incomplete) translations for Dutch, French, Galician, Japanese, Polish, and Russian.
|
||||||
- Fix having to long-press a message bubble on its corner to active the selection menu.
|
- Fix having to long-press a message bubble on its corner to active the selection menu.
|
||||||
- If enabled, read markers are automatically sent.
|
- If enabled, read markers are automatically sent.
|
||||||
- Highlight legacy quotes in text messages.
|
- Highlight legacy quotes in text messages.
|
||||||
|
- Fix Moxxy's app icon having a badge because of the foreground service.
|
||||||
|
- Make the notifications much prettier and compliant with Android 13.
|
||||||
|
- Prevent Moxxy from crashing on startup on a fresh device.
|
||||||
|
- Video thumbnails are now generated, if possible, after a video has been downloaded.
|
||||||
|
- The Moxxy APKs will now be signed by a different key stored on my YubiKey. You will have to uninstall and reinstall Moxxy. This will remove all your data.
|
||||||
|
- Moxxy now uses Android's direct share shortcuts.
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
# Android
|
# Android
|
||||||
pinnedJDK sdk
|
pinnedJDK sdk ktlint
|
||||||
scrcpy
|
scrcpy
|
||||||
|
|
||||||
# Flutter
|
# Flutter
|
||||||
|
@ -481,6 +481,12 @@ files:
|
|||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
jid: String
|
jid: String
|
||||||
|
- name: ExitConversationCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
conversationType: String
|
||||||
- name: SendChatStateCommand
|
- name: SendChatStateCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
implements:
|
implements:
|
||||||
|
@ -89,7 +89,7 @@ class AvatarService {
|
|||||||
final rs = GetIt.I.get<RosterService>();
|
final rs = GetIt.I.get<RosterService>();
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final originalConversation =
|
final originalConversation =
|
||||||
await cs.getConversationByJid(jid.toString(), accountJid);
|
await cs.getConversationByJid(jid.toString(), accountJid!);
|
||||||
final originalRoster = await rs.getRosterItemByJid(
|
final originalRoster = await rs.getRosterItemByJid(
|
||||||
jid.toString(),
|
jid.toString(),
|
||||||
accountJid,
|
accountJid,
|
||||||
@ -197,8 +197,9 @@ class AvatarService {
|
|||||||
/// Like [requestAvatar], but fetches and processes the avatar for our own account.
|
/// Like [requestAvatar], but fetches and processes the avatar for our own account.
|
||||||
Future<void> requestOwnAvatar() async {
|
Future<void> requestOwnAvatar() async {
|
||||||
final xss = GetIt.I.get<XmppStateService>();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final state = await xss.getXmppState();
|
final accountJid = await xss.getAccountJid();
|
||||||
final jid = JID.fromString(state.jid!);
|
final state = await xss.state;
|
||||||
|
final jid = JID.fromString(accountJid!);
|
||||||
|
|
||||||
if (_requestedInStream.contains(jid)) {
|
if (_requestedInStream.contains(jid)) {
|
||||||
return;
|
return;
|
||||||
|
@ -72,7 +72,7 @@ class BlocklistService {
|
|||||||
final removedItems = List<String>.empty(growable: true);
|
final removedItems = List<String>.empty(growable: true);
|
||||||
for (final item in blocklist) {
|
for (final item in blocklist) {
|
||||||
if (!_blocklist!.contains(item)) {
|
if (!_blocklist!.contains(item)) {
|
||||||
await _addBlocklistEntry(item, accountJid);
|
await _addBlocklistEntry(item, accountJid!);
|
||||||
_blocklist!.add(item);
|
_blocklist!.add(item);
|
||||||
newItems.add(item);
|
newItems.add(item);
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ class BlocklistService {
|
|||||||
// Diff the cache with the received blocklist
|
// Diff the cache with the received blocklist
|
||||||
for (final item in _blocklist!) {
|
for (final item in _blocklist!) {
|
||||||
if (!blocklist.contains(item)) {
|
if (!blocklist.contains(item)) {
|
||||||
await _removeBlocklistEntry(item, accountJid);
|
await _removeBlocklistEntry(item, accountJid!);
|
||||||
_blocklist!.remove(item);
|
_blocklist!.remove(item);
|
||||||
removedItems.add(item);
|
removedItems.add(item);
|
||||||
}
|
}
|
||||||
@ -146,7 +146,7 @@ class BlocklistService {
|
|||||||
_blocklist!.add(item);
|
_blocklist!.add(item);
|
||||||
newBlocks.add(item);
|
newBlocks.add(item);
|
||||||
|
|
||||||
await _addBlocklistEntry(item, accountJid);
|
await _addBlocklistEntry(item, accountJid!);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BlockPushType.unblock:
|
case BlockPushType.unblock:
|
||||||
@ -154,7 +154,7 @@ class BlocklistService {
|
|||||||
_blocklist!.removeWhere((i) => i == item);
|
_blocklist!.removeWhere((i) => i == item);
|
||||||
removedBlocks.add(item);
|
removedBlocks.add(item);
|
||||||
|
|
||||||
await _removeBlocklistEntry(item, accountJid);
|
await _removeBlocklistEntry(item, accountJid!);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -178,7 +178,7 @@ class BlocklistService {
|
|||||||
_blocklist!.add(jid);
|
_blocklist!.add(jid);
|
||||||
await _addBlocklistEntry(
|
await _addBlocklistEntry(
|
||||||
jid,
|
jid,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
return GetIt.I
|
return GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
@ -196,7 +196,7 @@ class BlocklistService {
|
|||||||
_blocklist!.remove(jid);
|
_blocklist!.remove(jid);
|
||||||
await _removeBlocklistEntry(
|
await _removeBlocklistEntry(
|
||||||
jid,
|
jid,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
return GetIt.I
|
return GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
|
@ -61,7 +61,7 @@ class ContactsService {
|
|||||||
FlutterContacts.removeListener(_onContactsDatabaseUpdate);
|
FlutterContacts.removeListener(_onContactsDatabaseUpdate);
|
||||||
|
|
||||||
await GetIt.I.get<RosterService>().removePseudoRosterItems(
|
await GetIt.I.get<RosterService>().removePseudoRosterItems(
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +218,7 @@ class ContactsService {
|
|||||||
// Remove the contact attributes from the conversation, if it existed
|
// Remove the contact attributes from the conversation, if it existed
|
||||||
final conversation = await cs.createOrUpdateConversation(
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
jid,
|
jid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
jid,
|
jid,
|
||||||
@ -284,7 +284,7 @@ class ContactsService {
|
|||||||
// Update a possibly existing conversation
|
// Update a possibly existing conversation
|
||||||
final conversation = await cs.createOrUpdateConversation(
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
contact.jid,
|
contact.jid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
contact.jid,
|
contact.jid,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
|
import 'package:moxxyv2/service/connectivity.dart';
|
||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
import 'package:moxxyv2/service/database/database.dart';
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
import 'package:moxxyv2/service/database/helpers.dart';
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
@ -28,6 +30,17 @@ class ConversationService {
|
|||||||
/// The lock for accessing _conversationCache
|
/// The lock for accessing _conversationCache
|
||||||
final Lock _lock = Lock();
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
final Logger _log = Logger('ConversationService');
|
||||||
|
|
||||||
|
String? _activeConversationJid;
|
||||||
|
|
||||||
|
String? get activeConversationJid => _activeConversationJid;
|
||||||
|
|
||||||
|
set activeConversationJid(String? jid) {
|
||||||
|
_log.finest('Setting activeConversationJid to $jid');
|
||||||
|
_activeConversationJid = jid;
|
||||||
|
}
|
||||||
|
|
||||||
/// When called with a JID [jid], then first, if non-null, [preRun] is
|
/// When called with a JID [jid], then first, if non-null, [preRun] is
|
||||||
/// executed.
|
/// executed.
|
||||||
/// Next, if a conversation with JID [jid] exists, [update] is called with
|
/// Next, if a conversation with JID [jid] exists, [update] is called with
|
||||||
@ -309,4 +322,36 @@ class ConversationService {
|
|||||||
final conversation = await getConversationByJid(jid.toString(), accountJid);
|
final conversation = await getConversationByJid(jid.toString(), accountJid);
|
||||||
return conversation?.encrypted ?? prefs.enableOmemoByDefault;
|
return conversation?.encrypted ?? prefs.enableOmemoByDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a chat state [state] to [jid], if certain pre-conditions are met:
|
||||||
|
/// - We have a network connection
|
||||||
|
/// - Sending chat markers/states are enabled
|
||||||
|
/// - [jid] != '' (not the self-chat)
|
||||||
|
/// [type] is the type of chat the chat state should be sent within.
|
||||||
|
Future<void> sendChatState(
|
||||||
|
ConversationType type,
|
||||||
|
String jid,
|
||||||
|
ChatState state,
|
||||||
|
) async {
|
||||||
|
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
|
|
||||||
|
// Only send chat states if the users wants to send them
|
||||||
|
if (!prefs.sendChatMarkers) return;
|
||||||
|
|
||||||
|
// Only send chat states when we're connected
|
||||||
|
// TODO(Unknown): Maybe queue it up intelligently
|
||||||
|
if (!(await GetIt.I.get<ConnectivityService>().hasConnection())) return;
|
||||||
|
|
||||||
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
|
||||||
|
if (jid != '') {
|
||||||
|
await conn
|
||||||
|
.getManagerById<ChatStateManager>(chatStateManager)!
|
||||||
|
.sendChatState(
|
||||||
|
state,
|
||||||
|
jid,
|
||||||
|
messageType: type.toMessageType(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,53 +57,53 @@ import 'package:sqflite_common/src/sql_builder.dart';
|
|||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
const List<DatabaseMigration<Database>> migrations = [
|
const List<Migration<Database>> migrations = [
|
||||||
DatabaseMigration(2, upgradeFromV1ToV2),
|
Migration(2, upgradeFromV1ToV2),
|
||||||
DatabaseMigration(3, upgradeFromV2ToV3),
|
Migration(3, upgradeFromV2ToV3),
|
||||||
DatabaseMigration(4, upgradeFromV3ToV4),
|
Migration(4, upgradeFromV3ToV4),
|
||||||
DatabaseMigration(5, upgradeFromV4ToV5),
|
Migration(5, upgradeFromV4ToV5),
|
||||||
DatabaseMigration(6, upgradeFromV5ToV6),
|
Migration(6, upgradeFromV5ToV6),
|
||||||
DatabaseMigration(7, upgradeFromV6ToV7),
|
Migration(7, upgradeFromV6ToV7),
|
||||||
DatabaseMigration(8, upgradeFromV7ToV8),
|
Migration(8, upgradeFromV7ToV8),
|
||||||
DatabaseMigration(9, upgradeFromV8ToV9),
|
Migration(9, upgradeFromV8ToV9),
|
||||||
DatabaseMigration(10, upgradeFromV9ToV10),
|
Migration(10, upgradeFromV9ToV10),
|
||||||
DatabaseMigration(11, upgradeFromV10ToV11),
|
Migration(11, upgradeFromV10ToV11),
|
||||||
DatabaseMigration(12, upgradeFromV11ToV12),
|
Migration(12, upgradeFromV11ToV12),
|
||||||
DatabaseMigration(13, upgradeFromV12ToV13),
|
Migration(13, upgradeFromV12ToV13),
|
||||||
DatabaseMigration(14, upgradeFromV13ToV14),
|
Migration(14, upgradeFromV13ToV14),
|
||||||
DatabaseMigration(15, upgradeFromV14ToV15),
|
Migration(15, upgradeFromV14ToV15),
|
||||||
DatabaseMigration(16, upgradeFromV15ToV16),
|
Migration(16, upgradeFromV15ToV16),
|
||||||
DatabaseMigration(17, upgradeFromV16ToV17),
|
Migration(17, upgradeFromV16ToV17),
|
||||||
DatabaseMigration(18, upgradeFromV17ToV18),
|
Migration(18, upgradeFromV17ToV18),
|
||||||
DatabaseMigration(19, upgradeFromV18ToV19),
|
Migration(19, upgradeFromV18ToV19),
|
||||||
DatabaseMigration(20, upgradeFromV19ToV20),
|
Migration(20, upgradeFromV19ToV20),
|
||||||
DatabaseMigration(21, upgradeFromV20ToV21),
|
Migration(21, upgradeFromV20ToV21),
|
||||||
DatabaseMigration(22, upgradeFromV21ToV22),
|
Migration(22, upgradeFromV21ToV22),
|
||||||
DatabaseMigration(23, upgradeFromV22ToV23),
|
Migration(23, upgradeFromV22ToV23),
|
||||||
DatabaseMigration(24, upgradeFromV23ToV24),
|
Migration(24, upgradeFromV23ToV24),
|
||||||
DatabaseMigration(25, upgradeFromV24ToV25),
|
Migration(25, upgradeFromV24ToV25),
|
||||||
DatabaseMigration(26, upgradeFromV25ToV26),
|
Migration(26, upgradeFromV25ToV26),
|
||||||
DatabaseMigration(27, upgradeFromV26ToV27),
|
Migration(27, upgradeFromV26ToV27),
|
||||||
DatabaseMigration(28, upgradeFromV27ToV28),
|
Migration(28, upgradeFromV27ToV28),
|
||||||
DatabaseMigration(29, upgradeFromV28ToV29),
|
Migration(29, upgradeFromV28ToV29),
|
||||||
DatabaseMigration(30, upgradeFromV29ToV30),
|
Migration(30, upgradeFromV29ToV30),
|
||||||
DatabaseMigration(31, upgradeFromV30ToV31),
|
Migration(31, upgradeFromV30ToV31),
|
||||||
DatabaseMigration(32, upgradeFromV31ToV32),
|
Migration(32, upgradeFromV31ToV32),
|
||||||
DatabaseMigration(33, upgradeFromV32ToV33),
|
Migration(33, upgradeFromV32ToV33),
|
||||||
DatabaseMigration(34, upgradeFromV33ToV34),
|
Migration(34, upgradeFromV33ToV34),
|
||||||
DatabaseMigration(35, upgradeFromV34ToV35),
|
Migration(35, upgradeFromV34ToV35),
|
||||||
DatabaseMigration(36, upgradeFromV35ToV36),
|
Migration(36, upgradeFromV35ToV36),
|
||||||
DatabaseMigration(37, upgradeFromV36ToV37),
|
Migration(37, upgradeFromV36ToV37),
|
||||||
DatabaseMigration(38, upgradeFromV37ToV38),
|
Migration(38, upgradeFromV37ToV38),
|
||||||
DatabaseMigration(39, upgradeFromV38ToV39),
|
Migration(39, upgradeFromV38ToV39),
|
||||||
DatabaseMigration(40, upgradeFromV39ToV40),
|
Migration(40, upgradeFromV39ToV40),
|
||||||
DatabaseMigration(41, upgradeFromV40ToV41),
|
Migration(41, upgradeFromV40ToV41),
|
||||||
DatabaseMigration(42, upgradeFromV41ToV42),
|
Migration(42, upgradeFromV41ToV42),
|
||||||
DatabaseMigration(43, upgradeFromV42ToV43),
|
Migration(43, upgradeFromV42ToV43),
|
||||||
DatabaseMigration(44, upgradeFromV43ToV44),
|
Migration(44, upgradeFromV43ToV44),
|
||||||
DatabaseMigration(45, upgradeFromV44ToV45),
|
Migration(45, upgradeFromV44ToV45),
|
||||||
DatabaseMigration(46, upgradeFromV45ToV46),
|
Migration(46, upgradeFromV45ToV46),
|
||||||
DatabaseMigration(47, upgradeFromV46ToV47),
|
Migration(47, upgradeFromV46ToV47),
|
||||||
];
|
];
|
||||||
|
|
||||||
class DatabaseService {
|
class DatabaseService {
|
||||||
@ -150,7 +150,7 @@ class DatabaseService {
|
|||||||
await db.execute('PRAGMA foreign_keys = ON');
|
await db.execute('PRAGMA foreign_keys = ON');
|
||||||
},
|
},
|
||||||
onUpgrade: (db, oldVersion, newVersion) async {
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
await runMigrations(_log, db, migrations, oldVersion);
|
await runMigrations(_log, db, migrations, oldVersion, 'database');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
/// A function to be called when a migration should be performed.
|
/// A function to be called when a migration should be performed.
|
||||||
typedef DatabaseMigrationCallback<T> = Future<void> Function(T);
|
typedef MigrationCallback<T> = Future<void> Function(T);
|
||||||
|
|
||||||
/// This class represents a single database migration.
|
/// This class represents a single database migration.
|
||||||
class DatabaseMigration<T> {
|
class Migration<T> {
|
||||||
const DatabaseMigration(this.version, this.migration);
|
const Migration(this.version, this.migration);
|
||||||
|
|
||||||
/// The version this migration upgrades the database to.
|
/// The version this migration upgrades the database to.
|
||||||
final int version;
|
final int version;
|
||||||
|
|
||||||
/// The migration callback. Called the the database version is less than [version].
|
/// The migration callback. Called the the database version is less than [version].
|
||||||
final DatabaseMigrationCallback<T> migration;
|
final MigrationCallback<T> migration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given the database [db] with the current version [version], goes through the list of
|
/// Given the migration [param], which is passed to every migration, with the current version
|
||||||
|
/// [version], goes through the list of
|
||||||
/// migrations [migrations] and applies all migrations with a version greater than
|
/// migrations [migrations] and applies all migrations with a version greater than
|
||||||
/// [version]. [migrations] is sorted before usage.
|
/// [version]. [migrations] is sorted before usage.
|
||||||
///
|
///
|
||||||
@ -23,22 +24,32 @@ class DatabaseMigration<T> {
|
|||||||
/// database argument, just pass in whatever (the tests use an integer).
|
/// database argument, just pass in whatever (the tests use an integer).
|
||||||
Future<void> runMigrations<T>(
|
Future<void> runMigrations<T>(
|
||||||
Logger log,
|
Logger log,
|
||||||
T db,
|
T param,
|
||||||
List<DatabaseMigration<T>> migrations,
|
List<Migration<T>> migrations,
|
||||||
int version,
|
int version,
|
||||||
) async {
|
String typeName, {
|
||||||
final sortedMigrations = List<DatabaseMigration<T>>.from(migrations)
|
Future<void> Function(int)? commitVersion,
|
||||||
|
}) async {
|
||||||
|
final sortedMigrations = List<Migration<T>>.from(migrations)
|
||||||
..sort(
|
..sort(
|
||||||
(a, b) => a.version.compareTo(b.version),
|
(a, b) => a.version.compareTo(b.version),
|
||||||
);
|
);
|
||||||
var currentVersion = version;
|
var currentVersion = version;
|
||||||
|
var hasRunMigration = false;
|
||||||
for (final migration in sortedMigrations) {
|
for (final migration in sortedMigrations) {
|
||||||
if (version < migration.version) {
|
if (version < migration.version) {
|
||||||
log.info(
|
log.info(
|
||||||
'Running database migration $currentVersion -> ${migration.version}',
|
'Running $typeName migration $currentVersion -> ${migration.version}',
|
||||||
);
|
);
|
||||||
await migration.migration(db);
|
await migration.migration(param);
|
||||||
currentVersion = migration.version;
|
currentVersion = migration.version;
|
||||||
|
hasRunMigration = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the version, if specified.
|
||||||
|
if (commitVersion != null && hasRunMigration) {
|
||||||
|
log.info('Committing migration version $currentVersion');
|
||||||
|
await commitVersion(currentVersion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -377,7 +377,7 @@ Future<void> upgradeFromV45ToV46(Database db) async {
|
|||||||
'${omemoTrustTable}_new',
|
'${omemoTrustTable}_new',
|
||||||
{
|
{
|
||||||
...trustItem,
|
...trustItem,
|
||||||
'accoutJid': accountJid,
|
'accountJid': accountJid,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import 'package:moxxmpp/moxxmpp.dart';
|
|||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/service/avatars.dart';
|
import 'package:moxxyv2/service/avatars.dart';
|
||||||
import 'package:moxxyv2/service/blocking.dart';
|
import 'package:moxxyv2/service/blocking.dart';
|
||||||
import 'package:moxxyv2/service/connectivity.dart';
|
|
||||||
import 'package:moxxyv2/service/contacts.dart';
|
import 'package:moxxyv2/service/contacts.dart';
|
||||||
import 'package:moxxyv2/service/conversation.dart';
|
import 'package:moxxyv2/service/conversation.dart';
|
||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
@ -20,6 +19,7 @@ import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
|||||||
import 'package:moxxyv2/service/httpfiletransfer/jobs.dart';
|
import 'package:moxxyv2/service/httpfiletransfer/jobs.dart';
|
||||||
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
||||||
import 'package:moxxyv2/service/language.dart';
|
import 'package:moxxyv2/service/language.dart';
|
||||||
|
import 'package:moxxyv2/service/lifecycle.dart';
|
||||||
import 'package:moxxyv2/service/message.dart';
|
import 'package:moxxyv2/service/message.dart';
|
||||||
import 'package:moxxyv2/service/notifications.dart';
|
import 'package:moxxyv2/service/notifications.dart';
|
||||||
import 'package:moxxyv2/service/omemo/omemo.dart';
|
import 'package:moxxyv2/service/omemo/omemo.dart';
|
||||||
@ -117,6 +117,7 @@ void setupBackgroundEventHandler() {
|
|||||||
EventTypeMatcher<FetchRecipientInformationCommand>(
|
EventTypeMatcher<FetchRecipientInformationCommand>(
|
||||||
performFetchRecipientInformation,
|
performFetchRecipientInformation,
|
||||||
),
|
),
|
||||||
|
EventTypeMatcher<ExitConversationCommand>(performConversationExited),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
GetIt.I.registerSingleton<EventHandler>(handler);
|
GetIt.I.registerSingleton<EventHandler>(handler);
|
||||||
@ -128,6 +129,17 @@ void setupBackgroundEventHandler() {
|
|||||||
Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
|
Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
|
||||||
final id = extra as String;
|
final id = extra as String;
|
||||||
|
|
||||||
|
// Set up the XMPP state
|
||||||
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
|
await xss.setAccountJid(command.jid, commit: false);
|
||||||
|
await xss.modifyXmppState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
jid: command.jid,
|
||||||
|
password: command.password,
|
||||||
|
),
|
||||||
|
commit: false,
|
||||||
|
);
|
||||||
|
|
||||||
GetIt.I.get<Logger>().fine('Performing login...');
|
GetIt.I.get<Logger>().fine('Performing login...');
|
||||||
final result = await GetIt.I.get<XmppService>().connectAwaitable(
|
final result = await GetIt.I.get<XmppService>().connectAwaitable(
|
||||||
ConnectionSettings(
|
ConnectionSettings(
|
||||||
@ -141,14 +153,25 @@ Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
|
|||||||
// ignore: avoid_dynamic_calls
|
// ignore: avoid_dynamic_calls
|
||||||
final xc = GetIt.I.get<XmppConnection>();
|
final xc = GetIt.I.get<XmppConnection>();
|
||||||
if (result.isType<bool>() && result.get<bool>()) {
|
if (result.isType<bool>() && result.get<bool>()) {
|
||||||
await GetIt.I.get<XmppStateService>().setAccountJid(command.jid);
|
// Persistently store the JID
|
||||||
|
await xss.setAccountJid(command.jid);
|
||||||
|
|
||||||
|
// Commit the XMPP state
|
||||||
|
await xss.commitXmppState(command.jid);
|
||||||
|
|
||||||
final preferences =
|
final preferences =
|
||||||
await GetIt.I.get<PreferencesService>().getPreferences();
|
await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
final settings = xc.connectionSettings;
|
final settings = xc.connectionSettings;
|
||||||
|
final state = await xss.state;
|
||||||
|
|
||||||
sendEvent(
|
sendEvent(
|
||||||
LoginSuccessfulEvent(
|
LoginSuccessfulEvent(
|
||||||
jid: settings.jid.toString(),
|
jid: settings.jid.toString(),
|
||||||
preStart: await _buildPreStartDoneEvent(preferences),
|
preStart: await _buildPreStartDoneEvent(
|
||||||
|
state,
|
||||||
|
command.jid,
|
||||||
|
preferences,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
id: id,
|
id: id,
|
||||||
);
|
);
|
||||||
@ -165,12 +188,10 @@ Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<PreStartDoneEvent> _buildPreStartDoneEvent(
|
Future<PreStartDoneEvent> _buildPreStartDoneEvent(
|
||||||
|
XmppState state,
|
||||||
|
String accountJid,
|
||||||
PreferencesState preferences,
|
PreferencesState preferences,
|
||||||
) async {
|
) async {
|
||||||
final xss = GetIt.I.get<XmppStateService>();
|
|
||||||
final accountJid = await xss.getAccountJid();
|
|
||||||
final state = await xss.getXmppState();
|
|
||||||
|
|
||||||
await GetIt.I.get<RosterService>().loadRosterFromDatabase(accountJid);
|
await GetIt.I.get<RosterService>().loadRosterFromDatabase(accountJid);
|
||||||
|
|
||||||
return PreStartDoneEvent(
|
return PreStartDoneEvent(
|
||||||
@ -203,11 +224,16 @@ Future<void> performPreStart(
|
|||||||
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
|
|
||||||
// Set the locale very early
|
// Set the locale very early
|
||||||
|
final logger = GetIt.I.get<Logger>();
|
||||||
GetIt.I.get<LanguageService>().defaultLocale = command.systemLocaleCode;
|
GetIt.I.get<LanguageService>().defaultLocale = command.systemLocaleCode;
|
||||||
if (preferences.languageLocaleCode == 'default') {
|
if (preferences.languageLocaleCode == 'default') {
|
||||||
LocaleSettings.setLocaleRaw(command.systemLocaleCode);
|
LocaleSettings.setLocaleRaw(command.systemLocaleCode);
|
||||||
|
logger.finest('Setting locale to default (${command.systemLocaleCode})');
|
||||||
} else {
|
} else {
|
||||||
LocaleSettings.setLocaleRaw(preferences.languageLocaleCode);
|
LocaleSettings.setLocaleRaw(preferences.languageLocaleCode);
|
||||||
|
logger.finest(
|
||||||
|
'Setting locale to configured language (${preferences.languageLocaleCode})',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await GetIt.I.get<NotificationsService>().configureNotificationI18n();
|
await GetIt.I.get<NotificationsService>().configureNotificationI18n();
|
||||||
GetIt.I.get<XmppService>().setNotificationText(
|
GetIt.I.get<XmppService>().setNotificationText(
|
||||||
@ -215,25 +241,23 @@ Future<void> performPreStart(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check if we have an account JID and, if we do, if we have login data.
|
// Check if we have an account JID and, if we do, if we have login data.
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getRawAccountJid();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final isLoggedIn = accountJid != null
|
final accountJid = await xss.getAccountJid();
|
||||||
? await GetIt.I.get<XmppService>().getConnectionSettings() != null
|
if (await xss.isLoggedIn(accountJid)) {
|
||||||
: false;
|
|
||||||
if (isLoggedIn) {
|
|
||||||
sendEvent(
|
sendEvent(
|
||||||
await _buildPreStartDoneEvent(preferences),
|
await _buildPreStartDoneEvent(
|
||||||
|
await xss.state,
|
||||||
|
accountJid!,
|
||||||
|
preferences,
|
||||||
|
),
|
||||||
id: id,
|
id: id,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
sendEvent(
|
sendEvent(
|
||||||
PreStartDoneEvent(
|
PreStartDoneEvent(
|
||||||
state: 'not_logged_in',
|
state: 'not_logged_in',
|
||||||
requestNotificationPermission: await GetIt.I
|
requestNotificationPermission: false,
|
||||||
.get<PermissionsService>()
|
excludeFromBatteryOptimisation: false,
|
||||||
.shouldRequestNotificationPermission(),
|
|
||||||
excludeFromBatteryOptimisation: await GetIt.I
|
|
||||||
.get<PermissionsService>()
|
|
||||||
.shouldRequestBatteryOptimisationExcemption(),
|
|
||||||
preferences: preferences,
|
preferences: preferences,
|
||||||
),
|
),
|
||||||
id: id,
|
id: id,
|
||||||
@ -253,7 +277,7 @@ Future<void> performAddConversation(
|
|||||||
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
await cs.createOrUpdateConversation(
|
await cs.createOrUpdateConversation(
|
||||||
command.jid,
|
command.jid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
create: () async {
|
create: () async {
|
||||||
// Create
|
// Create
|
||||||
final contactId = await css.getContactIdForJid(command.jid);
|
final contactId = await css.getContactIdForJid(command.jid);
|
||||||
@ -323,7 +347,7 @@ Future<void> performSetOpenConversation(
|
|||||||
if (command.jid != null && command.jid != '') {
|
if (command.jid != null && command.jid != '') {
|
||||||
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
|
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
|
||||||
command.jid!,
|
command.jid!,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,7 +367,7 @@ Future<void> performSendMessage(
|
|||||||
await xs.sendMessageCorrection(
|
await xs.sendMessageCorrection(
|
||||||
command.editSid!,
|
command.editSid!,
|
||||||
command.recipients.first,
|
command.recipients.first,
|
||||||
accountJid,
|
accountJid!,
|
||||||
command.body,
|
command.body,
|
||||||
command.editSid!,
|
command.editSid!,
|
||||||
command.recipients.first,
|
command.recipients.first,
|
||||||
@ -355,7 +379,7 @@ Future<void> performSendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await xs.sendMessage(
|
await xs.sendMessage(
|
||||||
accountJid: accountJid,
|
accountJid: accountJid!,
|
||||||
body: command.body,
|
body: command.body,
|
||||||
recipients: command.recipients,
|
recipients: command.recipients,
|
||||||
chatState: command.chatState.isNotEmpty
|
chatState: command.chatState.isNotEmpty
|
||||||
@ -390,11 +414,10 @@ Future<void> performSetCSIState(
|
|||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
// Tell the [XmppService] about the app state
|
// Tell the [XmppService] about the app state
|
||||||
GetIt.I.get<XmppService>().setAppState(command.active);
|
GetIt.I.get<LifecycleService>().isActive = command.active;
|
||||||
|
|
||||||
final conn = GetIt.I.get<XmppConnection>();
|
|
||||||
|
|
||||||
// Only send the CSI nonza when we're connected
|
// Only send the CSI nonza when we're connected
|
||||||
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
if (await conn.getConnectionState() != XmppConnectionState.connected) return;
|
if (await conn.getConnectionState() != XmppConnectionState.connected) return;
|
||||||
final csi = conn.getManagerById<CSIManager>(csiManager)!;
|
final csi = conn.getManagerById<CSIManager>(csiManager)!;
|
||||||
if (command.active) {
|
if (command.active) {
|
||||||
@ -435,11 +458,12 @@ Future<void> performSetPreferences(
|
|||||||
|
|
||||||
// TODO(Unknown): Maybe handle this in StickersService
|
// TODO(Unknown): Maybe handle this in StickersService
|
||||||
// If sticker visibility was changed, apply the settings to the PubSub node
|
// If sticker visibility was changed, apply the settings to the PubSub node
|
||||||
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final pm = GetIt.I
|
final pm = GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getManagerById<PubSubManager>(pubsubManager)!;
|
.getManagerById<PubSubManager>(pubsubManager)!;
|
||||||
final ownJid = JID.fromString(
|
final ownJid = JID.fromString(
|
||||||
(await GetIt.I.get<XmppStateService>().getXmppState()).jid!,
|
(await xss.state).jid!,
|
||||||
);
|
);
|
||||||
if (command.preferences.isStickersNodePublic &&
|
if (command.preferences.isStickersNodePublic &&
|
||||||
!oldPrefs.isStickersNodePublic) {
|
!oldPrefs.isStickersNodePublic) {
|
||||||
@ -528,7 +552,7 @@ Future<void> performAddContact(
|
|||||||
final jid = command.jid;
|
final jid = command.jid;
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final roster = GetIt.I.get<RosterService>();
|
final roster = GetIt.I.get<RosterService>();
|
||||||
final inRoster = await roster.isInRoster(jid, accountJid);
|
final inRoster = await roster.isInRoster(jid, accountJid!);
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
|
|
||||||
final conversation = await cs.getConversationByJid(jid, accountJid);
|
final conversation = await cs.getConversationByJid(jid, accountJid);
|
||||||
@ -649,7 +673,7 @@ Future<void> performRemoveContact(
|
|||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
|
|
||||||
// Remove from roster
|
// Remove from roster
|
||||||
await rs.removeFromRosterWrapper(command.jid, accountJid);
|
await rs.removeFromRosterWrapper(command.jid, accountJid!);
|
||||||
|
|
||||||
// Update the conversation
|
// Update the conversation
|
||||||
final conversation = await cs.getConversationByJid(command.jid, accountJid);
|
final conversation = await cs.getConversationByJid(command.jid, accountJid);
|
||||||
@ -674,7 +698,7 @@ Future<void> performRequestDownload(
|
|||||||
|
|
||||||
final message = await ms.updateMessage(
|
final message = await ms.updateMessage(
|
||||||
command.message.id,
|
command.message.id,
|
||||||
accountJid,
|
accountJid!,
|
||||||
isDownloading: true,
|
isDownloading: true,
|
||||||
);
|
);
|
||||||
sendEvent(MessageUpdatedEvent(message: message));
|
sendEvent(MessageUpdatedEvent(message: message));
|
||||||
@ -735,7 +759,7 @@ Future<void> performSetShareOnlineStatus(
|
|||||||
}) async {
|
}) async {
|
||||||
final rs = GetIt.I.get<RosterService>();
|
final rs = GetIt.I.get<RosterService>();
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final item = await rs.getRosterItemByJid(command.jid, accountJid);
|
final item = await rs.getRosterItemByJid(command.jid, accountJid!);
|
||||||
|
|
||||||
// TODO(Unknown): Maybe log
|
// TODO(Unknown): Maybe log
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
@ -774,7 +798,7 @@ Future<void> performCloseConversation(
|
|||||||
|
|
||||||
await cs.createOrUpdateConversation(
|
await cs.createOrUpdateConversation(
|
||||||
command.jid,
|
command.jid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
command.jid,
|
command.jid,
|
||||||
@ -794,25 +818,11 @@ Future<void> performSendChatState(
|
|||||||
SendChatStateCommand command, {
|
SendChatStateCommand command, {
|
||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
await GetIt.I.get<ConversationService>().sendChatState(
|
||||||
|
ConversationType.fromString(command.conversationType),
|
||||||
// Only send chat states if the users wants to send them
|
command.jid,
|
||||||
if (!prefs.sendChatMarkers) return;
|
ChatState.fromName(command.state),
|
||||||
|
);
|
||||||
// Only send chat states when we're connected
|
|
||||||
if (!(await GetIt.I.get<ConnectivityService>().hasConnection())) return;
|
|
||||||
|
|
||||||
final conn = GetIt.I.get<XmppConnection>();
|
|
||||||
|
|
||||||
if (command.jid != '') {
|
|
||||||
await conn
|
|
||||||
.getManagerById<ChatStateManager>(chatStateManager)!
|
|
||||||
.sendChatState(
|
|
||||||
ChatState.fromName(command.state),
|
|
||||||
command.jid,
|
|
||||||
messageType: command.conversationType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> performGetFeatures(
|
Future<void> performGetFeatures(
|
||||||
@ -854,7 +864,9 @@ Future<void> performSignOut(SignOutCommand command, {dynamic extra}) async {
|
|||||||
|
|
||||||
// Clear notifications
|
// Clear notifications
|
||||||
final accountJid = await xss.getAccountJid();
|
final accountJid = await xss.getAccountJid();
|
||||||
await GetIt.I.get<NotificationsService>().dismissAllNotifications(accountJid);
|
await GetIt.I
|
||||||
|
.get<NotificationsService>()
|
||||||
|
.dismissAllNotifications(accountJid!);
|
||||||
|
|
||||||
// Reset the current account JID.
|
// Reset the current account JID.
|
||||||
await xss.resetAccountJid();
|
await xss.resetAccountJid();
|
||||||
@ -867,7 +879,7 @@ Future<void> performSignOut(SignOutCommand command, {dynamic extra}) async {
|
|||||||
|
|
||||||
Future<void> performSendFiles(SendFilesCommand command, {dynamic extra}) async {
|
Future<void> performSendFiles(SendFilesCommand command, {dynamic extra}) async {
|
||||||
await GetIt.I.get<XmppService>().sendFiles(
|
await GetIt.I.get<XmppService>().sendFiles(
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
command.paths,
|
command.paths,
|
||||||
command.recipients,
|
command.recipients,
|
||||||
);
|
);
|
||||||
@ -882,7 +894,7 @@ Future<void> performSetMuteState(
|
|||||||
|
|
||||||
final conversation = await cs.createOrUpdateConversation(
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
command.jid,
|
command.jid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
command.jid,
|
command.jid,
|
||||||
@ -958,7 +970,7 @@ Future<void> performSetOmemoEnabled(
|
|||||||
|
|
||||||
await cs.createOrUpdateConversation(
|
await cs.createOrUpdateConversation(
|
||||||
command.jid,
|
command.jid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
command.jid,
|
command.jid,
|
||||||
@ -1018,7 +1030,7 @@ Future<void> performMessageRetraction(
|
|||||||
}) async {
|
}) async {
|
||||||
await GetIt.I.get<MessageService>().retractMessage(
|
await GetIt.I.get<MessageService>().retractMessage(
|
||||||
command.conversationJid,
|
command.conversationJid,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
command.originId,
|
command.originId,
|
||||||
'',
|
'',
|
||||||
true,
|
true,
|
||||||
@ -1046,7 +1058,7 @@ Future<void> performMarkConversationAsRead(
|
|||||||
// Update the database
|
// Update the database
|
||||||
final conversation = await cs.createOrUpdateConversation(
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
command.conversationJid,
|
command.conversationJid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
command.conversationJid,
|
command.conversationJid,
|
||||||
@ -1070,7 +1082,7 @@ Future<void> performMarkConversationAsRead(
|
|||||||
// Dismiss notifications for that chat
|
// Dismiss notifications for that chat
|
||||||
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
|
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
|
||||||
command.conversationJid,
|
command.conversationJid,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1081,7 +1093,7 @@ Future<void> performMarkMessageAsRead(
|
|||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
await GetIt.I.get<MessageService>().markMessageAsRead(
|
await GetIt.I.get<MessageService>().markMessageAsRead(
|
||||||
command.id,
|
command.id,
|
||||||
accountJid,
|
accountJid!,
|
||||||
command.sendMarker,
|
command.sendMarker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1094,7 +1106,7 @@ Future<void> performAddMessageReaction(
|
|||||||
final rs = GetIt.I.get<ReactionsService>();
|
final rs = GetIt.I.get<ReactionsService>();
|
||||||
final msg = await rs.addNewReaction(
|
final msg = await rs.addNewReaction(
|
||||||
command.id,
|
command.id,
|
||||||
accountJid,
|
accountJid!,
|
||||||
accountJid,
|
accountJid,
|
||||||
command.emoji,
|
command.emoji,
|
||||||
);
|
);
|
||||||
@ -1141,7 +1153,7 @@ Future<void> performRemoveMessageReaction(
|
|||||||
final rs = GetIt.I.get<ReactionsService>();
|
final rs = GetIt.I.get<ReactionsService>();
|
||||||
final msg = await rs.removeReaction(
|
final msg = await rs.removeReaction(
|
||||||
command.id,
|
command.id,
|
||||||
accountJid,
|
accountJid!,
|
||||||
accountJid,
|
accountJid,
|
||||||
command.emoji,
|
command.emoji,
|
||||||
);
|
);
|
||||||
@ -1217,7 +1229,7 @@ Future<void> performSendSticker(
|
|||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
await GetIt.I.get<XmppService>().sendMessage(
|
await GetIt.I.get<XmppService>().sendMessage(
|
||||||
accountJid: await GetIt.I.get<XmppStateService>().getAccountJid(),
|
accountJid: (await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
body: command.sticker.desc,
|
body: command.sticker.desc,
|
||||||
recipients: [command.recipient],
|
recipients: [command.recipient],
|
||||||
sticker: command.sticker,
|
sticker: command.sticker,
|
||||||
@ -1332,7 +1344,8 @@ Future<void> performGetBlocklist(
|
|||||||
}) async {
|
}) async {
|
||||||
final id = extra as String;
|
final id = extra as String;
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final result = await GetIt.I.get<BlocklistService>().getBlocklist(accountJid);
|
final result =
|
||||||
|
await GetIt.I.get<BlocklistService>().getBlocklist(accountJid!);
|
||||||
sendEvent(
|
sendEvent(
|
||||||
GetBlocklistResultEvent(
|
GetBlocklistResultEvent(
|
||||||
entries: result,
|
entries: result,
|
||||||
@ -1349,7 +1362,7 @@ Future<void> performGetPagedMessages(
|
|||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final result = await GetIt.I.get<MessageService>().getPaginatedMessagesForJid(
|
final result = await GetIt.I.get<MessageService>().getPaginatedMessagesForJid(
|
||||||
command.conversationJid,
|
command.conversationJid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
command.olderThan,
|
command.olderThan,
|
||||||
command.timestamp,
|
command.timestamp,
|
||||||
);
|
);
|
||||||
@ -1370,7 +1383,7 @@ Future<void> performGetPagedSharedMedia(
|
|||||||
final result =
|
final result =
|
||||||
await GetIt.I.get<MessageService>().getPaginatedSharedMediaMessagesForJid(
|
await GetIt.I.get<MessageService>().getPaginatedSharedMediaMessagesForJid(
|
||||||
command.conversationJid,
|
command.conversationJid,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
command.olderThan,
|
command.olderThan,
|
||||||
command.timestamp,
|
command.timestamp,
|
||||||
);
|
);
|
||||||
@ -1392,7 +1405,7 @@ Future<void> performGetReactions(
|
|||||||
final reactionsRaw =
|
final reactionsRaw =
|
||||||
await GetIt.I.get<ReactionsService>().getReactionsForMessage(
|
await GetIt.I.get<ReactionsService>().getReactionsForMessage(
|
||||||
command.id,
|
command.id,
|
||||||
accountJid,
|
accountJid!,
|
||||||
);
|
);
|
||||||
final reactionsMap = <String, List<String>>{};
|
final reactionsMap = <String, List<String>>{};
|
||||||
for (final reaction in reactionsRaw) {
|
for (final reaction in reactionsRaw) {
|
||||||
@ -1462,7 +1475,7 @@ Future<void> performOldMediaFileDeletion(
|
|||||||
newUsage: await GetIt.I.get<StorageService>().computeUsedMediaStorage(),
|
newUsage: await GetIt.I.get<StorageService>().computeUsedMediaStorage(),
|
||||||
conversations: (await GetIt.I
|
conversations: (await GetIt.I
|
||||||
.get<ConversationService>()
|
.get<ConversationService>()
|
||||||
.loadConversations(accountJid))
|
.loadConversations(accountJid!))
|
||||||
.where((c) => c.open)
|
.where((c) => c.open)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
@ -1551,7 +1564,7 @@ Future<void> performJoinGroupchat(
|
|||||||
final nick = command.nick;
|
final nick = command.nick;
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
final conversation = await cs.getConversationByJid(jid, accountJid);
|
final conversation = await cs.getConversationByJid(jid, accountJid!);
|
||||||
if (conversation != null) {
|
if (conversation != null) {
|
||||||
await cs.createOrUpdateConversation(
|
await cs.createOrUpdateConversation(
|
||||||
jid,
|
jid,
|
||||||
@ -1638,7 +1651,7 @@ Future<void> performFetchRecipientInformation(
|
|||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
for (final jid in command.jids) {
|
for (final jid in command.jids) {
|
||||||
// First try to find a roster item
|
// First try to find a roster item
|
||||||
final rosterItem = await rs.getRosterItemByJid(jid, accountJid);
|
final rosterItem = await rs.getRosterItemByJid(jid, accountJid!);
|
||||||
if (rosterItem != null) {
|
if (rosterItem != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SendFilesRecipient(
|
SendFilesRecipient(
|
||||||
@ -1673,3 +1686,19 @@ Future<void> performFetchRecipientInformation(
|
|||||||
id: extra as String,
|
id: extra as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> performConversationExited(
|
||||||
|
ExitConversationCommand command, {
|
||||||
|
dynamic extra,
|
||||||
|
}) async {
|
||||||
|
// Send a gone marker according to the specified rules
|
||||||
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
|
await cs.sendChatState(
|
||||||
|
ConversationType.fromString(command.conversationType),
|
||||||
|
cs.activeConversationJid!,
|
||||||
|
ChatState.gone,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset the active conversation
|
||||||
|
cs.activeConversationJid = null;
|
||||||
|
}
|
||||||
|
@ -11,6 +11,7 @@ import 'package:moxxyv2/service/database/database.dart';
|
|||||||
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
||||||
import 'package:moxxyv2/service/not_specified.dart';
|
import 'package:moxxyv2/service/not_specified.dart';
|
||||||
import 'package:moxxyv2/shared/models/file_metadata.dart';
|
import 'package:moxxyv2/shared/models/file_metadata.dart';
|
||||||
|
import 'package:moxxyv2/shared/thumbnails/helpers.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:sqflite_common/sql.dart';
|
import 'package:sqflite_common/sql.dart';
|
||||||
|
|
||||||
@ -323,6 +324,20 @@ class FilesService {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
_log.warning('Failed to remove file ${metadata.path!}: $ex');
|
_log.warning('Failed to remove file ${metadata.path!}: $ex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata.mimeType?.startsWith('video/') ?? false) {
|
||||||
|
final thumbnailPath = await getVideoThumbnailPath(metadata.path!);
|
||||||
|
final thumbnailFile = File(thumbnailPath);
|
||||||
|
if (thumbnailFile.existsSync()) {
|
||||||
|
try {
|
||||||
|
await thumbnailFile.delete();
|
||||||
|
} catch (ex) {
|
||||||
|
_log.warning(
|
||||||
|
'Failed to remove thumbnail file $thumbnailPath: $ex',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_log.info('Not removing file as there is no path associated with it');
|
_log.info('Not removing file as there is no path associated with it');
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import 'package:moxxyv2/service/service.dart';
|
|||||||
import 'package:moxxyv2/shared/error_types.dart';
|
import 'package:moxxyv2/shared/error_types.dart';
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
|
import 'package:moxxyv2/shared/thumbnails/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/warning_types.dart';
|
import 'package:moxxyv2/shared/warning_types.dart';
|
||||||
import 'package:path/path.dart' as pathlib;
|
import 'package:path/path.dart' as pathlib;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@ -545,21 +546,33 @@ class HttpFileTransferService {
|
|||||||
mediaWidth = imageSize?.width.toInt();
|
mediaWidth = imageSize?.width.toInt();
|
||||||
mediaHeight = imageSize?.height.toInt();
|
mediaHeight = imageSize?.height.toInt();
|
||||||
} else if (mime.startsWith('video/')) {
|
} else if (mime.startsWith('video/')) {
|
||||||
/*
|
if (canGenerateVideoThumbnail(mime)) {
|
||||||
// Generate thumbnail
|
try {
|
||||||
final thumbnailPath = await getVideoThumbnailPath(
|
// Generate thumbnail
|
||||||
downloadedPath,
|
final thumbnailPath = await maybeGenerateVideoThumbnail(
|
||||||
job.conversationJid,
|
downloadedPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find out the dimensions
|
if (thumbnailPath != null) {
|
||||||
final imageSize = await getImageSizeFromPath(thumbnailPath);
|
// Find out the dimensions
|
||||||
if (imageSize == null) {
|
final imageSize = await getImageSizeFromPath(thumbnailPath);
|
||||||
_log.warning('Failed to get image size for $downloadedPath ($thumbnailPath)');
|
if (imageSize == null) {
|
||||||
|
_log.warning(
|
||||||
|
'Failed to get image size for $downloadedPath ($thumbnailPath)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaWidth = imageSize?.width.toInt();
|
||||||
|
mediaHeight = imageSize?.height.toInt();
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
_log.warning('Failed to generate thumbnail for $downloadedPath');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_log.info(
|
||||||
|
'Not generating thumbnail for $downloadedPath because canGenerateVideoThumbnail returned false',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaWidth = imageSize?.width.toInt();
|
|
||||||
mediaHeight = imageSize?.height.toInt();*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,14 +629,19 @@ class HttpFileTransferService {
|
|||||||
cs.setConversation(updatedConversation);
|
cs.setConversation(updatedConversation);
|
||||||
|
|
||||||
// Show a notification
|
// Show a notification
|
||||||
if (notification.shouldShowNotification(msg.conversationJid) &&
|
final shouldShowNotification =
|
||||||
job.shouldShowNotification) {
|
notification.shouldShowNotification(msg.conversationJid);
|
||||||
|
if (shouldShowNotification && job.shouldShowNotification) {
|
||||||
_log.finest('Creating notification with bigPicture $downloadedPath');
|
_log.finest('Creating notification with bigPicture $downloadedPath');
|
||||||
await notification.updateNotification(
|
await notification.updateOrShowNotification(
|
||||||
updatedConversation,
|
updatedConversation,
|
||||||
msg,
|
msg,
|
||||||
job.accountJid,
|
job.accountJid,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
_log.finest(
|
||||||
|
'Not creating or updating notification for $downloadedPath: notification.shouldShowNotification=$shouldShowNotification, job.shouldShowNotification=${job.shouldShowNotification}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendEvent(ConversationUpdatedEvent(conversation: updatedConversation));
|
sendEvent(ConversationUpdatedEvent(conversation: updatedConversation));
|
||||||
|
16
lib/service/lifecycle.dart
Normal file
16
lib/service/lifecycle.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
/// Service which provides other services with information about the state of
|
||||||
|
/// the app, i.e. if it's in the foreground, minimized, ...
|
||||||
|
class LifecycleService {
|
||||||
|
final Logger _log = Logger('LifecycleService');
|
||||||
|
|
||||||
|
/// Flag indicating whether the app is currently active, i.e. in the foreground (true),
|
||||||
|
/// or inactive (false).
|
||||||
|
bool _active = false;
|
||||||
|
bool get isActive => _active;
|
||||||
|
set isActive(bool flag) {
|
||||||
|
_log.finest('Setting isActive to $flag');
|
||||||
|
_active = flag;
|
||||||
|
}
|
||||||
|
}
|
@ -37,9 +37,10 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
|||||||
Future<RosterCacheLoadResult> loadRosterCache() async {
|
Future<RosterCacheLoadResult> loadRosterCache() async {
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final rs = GetIt.I.get<RosterService>();
|
final rs = GetIt.I.get<RosterService>();
|
||||||
|
final state = await GetIt.I.get<XmppStateService>().state;
|
||||||
return RosterCacheLoadResult(
|
return RosterCacheLoadResult(
|
||||||
(await GetIt.I.get<XmppStateService>().getXmppState()).lastRosterVersion,
|
state.lastRosterVersion,
|
||||||
(await rs.getRoster(accountJid))
|
(await rs.getRoster(accountJid!))
|
||||||
.map(
|
.map(
|
||||||
(item) => XmppRosterItem(
|
(item) => XmppRosterItem(
|
||||||
jid: item.jid,
|
jid: item.jid,
|
||||||
@ -71,14 +72,14 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
|||||||
|
|
||||||
// Remove stale items
|
// Remove stale items
|
||||||
for (final jid in removed) {
|
for (final jid in removed) {
|
||||||
await rs.removeRosterItem(jid, accountJid);
|
await rs.removeRosterItem(jid, accountJid!);
|
||||||
await updateConversation(jid, accountJid, true);
|
await updateConversation(jid, accountJid, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new roster items
|
// Create new roster items
|
||||||
final rosterAdded = List<RosterItem>.empty(growable: true);
|
final rosterAdded = List<RosterItem>.empty(growable: true);
|
||||||
for (final item in added) {
|
for (final item in added) {
|
||||||
final exists = await rs.getRosterItemByJid(item.jid, accountJid) != null;
|
final exists = await rs.getRosterItemByJid(item.jid, accountJid!) != null;
|
||||||
// Skip adding items twice
|
// Skip adding items twice
|
||||||
if (exists) continue;
|
if (exists) continue;
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
|||||||
// Update modified items
|
// Update modified items
|
||||||
final rosterModified = List<RosterItem>.empty(growable: true);
|
final rosterModified = List<RosterItem>.empty(growable: true);
|
||||||
for (final item in modified) {
|
for (final item in modified) {
|
||||||
final ritem = await rs.getRosterItemByJid(item.jid, accountJid);
|
final ritem = await rs.getRosterItemByJid(item.jid, accountJid!);
|
||||||
if (ritem == null) {
|
if (ritem == null) {
|
||||||
//_log.warning('Could not find roster item with JID $jid during update');
|
//_log.warning('Could not find roster item with JID $jid during update');
|
||||||
continue;
|
continue;
|
||||||
|
@ -30,7 +30,8 @@ class MoxxyStreamManagementManager extends StreamManagementManager {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> loadState() async {
|
Future<void> loadState() async {
|
||||||
final state = await GetIt.I.get<XmppStateService>().getXmppState();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
|
final state = await xss.state;
|
||||||
if (state.smState != null) {
|
if (state.smState != null) {
|
||||||
await setState(state.smState!);
|
await setState(state.smState!);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
|
import 'package:moxxyv2/service/pigeon/api.g.dart';
|
||||||
|
import 'package:moxxyv2/shared/constants.dart';
|
||||||
|
|
||||||
|
/// Recreate all notification channels to apply settings that cannot be applied after the notification
|
||||||
|
/// channel has been created.
|
||||||
|
Future<void> upgradeV1ToV2NonDb(int _) async {
|
||||||
|
// Ensure that we can use the device locale
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
LocaleSettings.useDeviceLocale();
|
||||||
|
|
||||||
|
final api = MoxxyApi();
|
||||||
|
|
||||||
|
// Remove all notification channels, so that we can recreate them
|
||||||
|
await api.deleteNotificationChannels([
|
||||||
|
'FOREGROUND_DEFAULT',
|
||||||
|
'message_channel',
|
||||||
|
'warning_channel',
|
||||||
|
// Not sure where this one comes from
|
||||||
|
'warning',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set up notification groups
|
||||||
|
await api.createNotificationGroups(
|
||||||
|
[
|
||||||
|
NotificationGroup(
|
||||||
|
id: messageNotificationGroupId,
|
||||||
|
description: 'Chat messages',
|
||||||
|
),
|
||||||
|
NotificationGroup(
|
||||||
|
id: warningNotificationChannelId,
|
||||||
|
description: 'Warnings',
|
||||||
|
),
|
||||||
|
NotificationGroup(
|
||||||
|
id: foregroundServiceNotificationGroupId,
|
||||||
|
description: 'Foreground service',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up the notitifcation channels.
|
||||||
|
await api.createNotificationChannels([
|
||||||
|
NotificationChannel(
|
||||||
|
title: t.notifications.channels.messagesChannelName,
|
||||||
|
description: t.notifications.channels.messagesChannelDescription,
|
||||||
|
id: messageNotificationChannelId,
|
||||||
|
importance: NotificationChannelImportance.HIGH,
|
||||||
|
showBadge: true,
|
||||||
|
vibration: true,
|
||||||
|
enableLights: true,
|
||||||
|
),
|
||||||
|
NotificationChannel(
|
||||||
|
title: t.notifications.channels.warningChannelName,
|
||||||
|
description: t.notifications.channels.warningChannelDescription,
|
||||||
|
id: warningNotificationChannelId,
|
||||||
|
importance: NotificationChannelImportance.DEFAULT,
|
||||||
|
showBadge: false,
|
||||||
|
vibration: true,
|
||||||
|
enableLights: false,
|
||||||
|
),
|
||||||
|
// The foreground notification channel is only required on Android
|
||||||
|
if (Platform.isAndroid)
|
||||||
|
NotificationChannel(
|
||||||
|
title: t.notifications.channels.serviceChannelName,
|
||||||
|
description: t.notifications.channels.serviceChannelDescription,
|
||||||
|
id: foregroundServiceNotificationChannelId,
|
||||||
|
importance: NotificationChannelImportance.MIN,
|
||||||
|
showBadge: false,
|
||||||
|
vibration: false,
|
||||||
|
enableLights: false,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
@ -1,28 +1,31 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
import 'package:moxlib/moxlib.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/service/conversation.dart';
|
import 'package:moxxyv2/service/conversation.dart';
|
||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
import 'package:moxxyv2/service/database/database.dart';
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
|
import 'package:moxxyv2/service/lifecycle.dart';
|
||||||
import 'package:moxxyv2/service/message.dart';
|
import 'package:moxxyv2/service/message.dart';
|
||||||
|
import 'package:moxxyv2/service/pigeon/api.g.dart' as api;
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/service/xmpp.dart';
|
import 'package:moxxyv2/service/xmpp.dart';
|
||||||
import 'package:moxxyv2/service/xmpp_state.dart';
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
|
import 'package:moxxyv2/shared/constants.dart';
|
||||||
import 'package:moxxyv2/shared/error_types.dart';
|
import 'package:moxxyv2/shared/error_types.dart';
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/models/conversation.dart' as modelc;
|
import 'package:moxxyv2/shared/models/conversation.dart' as modelc;
|
||||||
import 'package:moxxyv2/shared/models/message.dart' as modelm;
|
import 'package:moxxyv2/shared/models/message.dart' as modelm;
|
||||||
import 'package:moxxyv2/shared/models/notification.dart' as modeln;
|
import 'package:moxxyv2/shared/models/notification.dart' as modeln;
|
||||||
|
import 'package:moxxyv2/shared/thumbnails/helpers.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
const _maxNotificationId = 2147483647;
|
const _maxNotificationId = 2147483647;
|
||||||
const _messageChannelKey = 'message_channel';
|
|
||||||
const _warningChannelKey = 'warning_channel';
|
|
||||||
|
|
||||||
/// Message payload keys.
|
/// Message payload keys.
|
||||||
const _conversationJidKey = 'conversationJid';
|
const _conversationJidKey = 'conversationJid';
|
||||||
@ -31,14 +34,27 @@ const _conversationTitleKey = 'title';
|
|||||||
const _conversationAvatarKey = 'avatarPath';
|
const _conversationAvatarKey = 'avatarPath';
|
||||||
|
|
||||||
class NotificationsService {
|
class NotificationsService {
|
||||||
|
NotificationsService() {
|
||||||
|
_eventStream = _channel
|
||||||
|
.receiveBroadcastStream()
|
||||||
|
.cast<Object>()
|
||||||
|
.map(api.NotificationEvent.decode);
|
||||||
|
}
|
||||||
|
|
||||||
/// Logging.
|
/// Logging.
|
||||||
final Logger _log = Logger('NotificationsService');
|
final Logger _log = Logger('NotificationsService');
|
||||||
|
|
||||||
|
/// The Pigeon channel to the native side
|
||||||
|
final api.MoxxyApi _api = api.MoxxyApi();
|
||||||
|
final EventChannel _channel =
|
||||||
|
const EventChannel('org.moxxy.moxxyv2/notification_stream');
|
||||||
|
late final Stream<api.NotificationEvent> _eventStream;
|
||||||
|
|
||||||
/// Called when something happens to the notification, i.e. the actions are triggered or
|
/// Called when something happens to the notification, i.e. the actions are triggered or
|
||||||
/// the notification has been tapped.
|
/// the notification has been tapped.
|
||||||
Future<void> onNotificationEvent(NotificationEvent event) async {
|
Future<void> onNotificationEvent(api.NotificationEvent event) async {
|
||||||
final conversationJid = event.extra![_conversationJidKey]!;
|
final conversationJid = event.extra![_conversationJidKey]!;
|
||||||
if (event.type == NotificationEventType.open) {
|
if (event.type == api.NotificationEventType.open) {
|
||||||
// The notification has been tapped
|
// The notification has been tapped
|
||||||
sendEvent(
|
sendEvent(
|
||||||
MessageNotificationTappedEvent(
|
MessageNotificationTappedEvent(
|
||||||
@ -47,12 +63,12 @@ class NotificationsService {
|
|||||||
avatarPath: event.extra![_conversationAvatarKey]!,
|
avatarPath: event.extra![_conversationAvatarKey]!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (event.type == NotificationEventType.markAsRead) {
|
} else if (event.type == api.NotificationEventType.markAsRead) {
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
// Mark the message as read
|
// Mark the message as read
|
||||||
await GetIt.I.get<MessageService>().markMessageAsRead(
|
await GetIt.I.get<MessageService>().markMessageAsRead(
|
||||||
event.extra![_messageIdKey]!,
|
event.extra![_messageIdKey]!,
|
||||||
accountJid,
|
accountJid!,
|
||||||
// [XmppService.sendReadMarker] will check whether the *SHOULD* send
|
// [XmppService.sendReadMarker] will check whether the *SHOULD* send
|
||||||
// the marker, i.e. if the privacy settings allow it.
|
// the marker, i.e. if the privacy settings allow it.
|
||||||
true,
|
true,
|
||||||
@ -83,7 +99,7 @@ class NotificationsService {
|
|||||||
|
|
||||||
// Clear notifications
|
// Clear notifications
|
||||||
await dismissNotificationsByJid(conversationJid, accountJid);
|
await dismissNotificationsByJid(conversationJid, accountJid);
|
||||||
} else if (event.type == NotificationEventType.reply) {
|
} else if (event.type == api.NotificationEventType.reply) {
|
||||||
// Save this as a notification so that we can display it later
|
// Save this as a notification so that we can display it later
|
||||||
assert(
|
assert(
|
||||||
event.payload != null,
|
event.payload != null,
|
||||||
@ -93,7 +109,7 @@ class NotificationsService {
|
|||||||
final notification = modeln.Notification(
|
final notification = modeln.Notification(
|
||||||
event.id,
|
event.id,
|
||||||
conversationJid,
|
conversationJid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -119,8 +135,8 @@ class NotificationsService {
|
|||||||
/// Configures the translatable strings on the native side
|
/// Configures the translatable strings on the native side
|
||||||
/// using locale is currently configured.
|
/// using locale is currently configured.
|
||||||
Future<void> configureNotificationI18n() async {
|
Future<void> configureNotificationI18n() async {
|
||||||
await MoxplatformPlugin.notifications.setI18n(
|
await _api.setNotificationI18n(
|
||||||
NotificationI18nData(
|
api.NotificationI18nData(
|
||||||
reply: t.notifications.message.reply,
|
reply: t.notifications.message.reply,
|
||||||
markAsRead: t.notifications.message.markAsRead,
|
markAsRead: t.notifications.message.markAsRead,
|
||||||
you: t.messages.you,
|
you: t.messages.you,
|
||||||
@ -129,32 +145,68 @@ class NotificationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
|
// Set up notification groups
|
||||||
|
await _api.createNotificationGroups(
|
||||||
|
[
|
||||||
|
api.NotificationGroup(
|
||||||
|
id: messageNotificationGroupId,
|
||||||
|
description: 'Chat messages',
|
||||||
|
),
|
||||||
|
api.NotificationGroup(
|
||||||
|
id: warningNotificationChannelId,
|
||||||
|
description: 'Warnings',
|
||||||
|
),
|
||||||
|
api.NotificationGroup(
|
||||||
|
id: foregroundServiceNotificationGroupId,
|
||||||
|
description: 'Foreground service',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Set up the notitifcation channels.
|
// Set up the notitifcation channels.
|
||||||
await MoxplatformPlugin.notifications.createNotificationChannel(
|
await _api.createNotificationChannels([
|
||||||
t.notifications.channels.messagesChannelName,
|
api.NotificationChannel(
|
||||||
t.notifications.channels.messagesChannelDescription,
|
title: t.notifications.channels.messagesChannelName,
|
||||||
_messageChannelKey,
|
description: t.notifications.channels.messagesChannelDescription,
|
||||||
true,
|
id: messageNotificationChannelId,
|
||||||
);
|
importance: api.NotificationChannelImportance.HIGH,
|
||||||
await MoxplatformPlugin.notifications.createNotificationChannel(
|
showBadge: true,
|
||||||
t.notifications.channels.warningChannelName,
|
vibration: true,
|
||||||
t.notifications.channels.warningChannelDescription,
|
enableLights: true,
|
||||||
_warningChannelKey,
|
),
|
||||||
false,
|
api.NotificationChannel(
|
||||||
);
|
title: t.notifications.channels.warningChannelName,
|
||||||
|
description: t.notifications.channels.warningChannelDescription,
|
||||||
|
id: warningNotificationChannelId,
|
||||||
|
importance: api.NotificationChannelImportance.DEFAULT,
|
||||||
|
showBadge: false,
|
||||||
|
vibration: true,
|
||||||
|
enableLights: false,
|
||||||
|
),
|
||||||
|
// The foreground notification channel is only required on Android
|
||||||
|
if (Platform.isAndroid)
|
||||||
|
api.NotificationChannel(
|
||||||
|
title: t.notifications.channels.serviceChannelName,
|
||||||
|
description: t.notifications.channels.serviceChannelDescription,
|
||||||
|
id: foregroundServiceNotificationChannelId,
|
||||||
|
importance: api.NotificationChannelImportance.MIN,
|
||||||
|
showBadge: false,
|
||||||
|
vibration: false,
|
||||||
|
enableLights: false,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
// Configure i18n
|
// Configure i18n
|
||||||
await configureNotificationI18n();
|
await configureNotificationI18n();
|
||||||
|
|
||||||
// Listen to notification events
|
// Listen to notification events
|
||||||
MoxplatformPlugin.notifications
|
_eventStream.listen(onNotificationEvent);
|
||||||
.getEventStream()
|
|
||||||
.listen(onNotificationEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if a notification should be shown. false otherwise.
|
/// Returns true if a notification should be shown. false otherwise.
|
||||||
bool shouldShowNotification(String jid) {
|
bool shouldShowNotification(String jid) {
|
||||||
return GetIt.I.get<XmppService>().getCurrentlyOpenedChatJid() != jid;
|
return GetIt.I.get<ConversationService>().activeConversationJid != jid ||
|
||||||
|
!GetIt.I.get<LifecycleService>().isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queries the notifications for the conversation [jid] from the database.
|
/// Queries the notifications for the conversation [jid] from the database.
|
||||||
@ -215,12 +267,28 @@ class NotificationsService {
|
|||||||
'File metadata has path but no mime type',
|
'File metadata has path but no mime type',
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Use the resource (nick) when the chat is a groupchat
|
// Use the resource (nick) when the chat is a groupchat
|
||||||
final senderJid = m.senderJid;
|
final senderJid = m.senderJid;
|
||||||
final senderTitle = c.isGroupchat
|
final senderTitle = c.isGroupchat
|
||||||
? senderJid.resource
|
? senderJid.resource
|
||||||
: await c.titleWithOptionalContactService;
|
: await c.titleWithOptionalContactService;
|
||||||
|
|
||||||
|
// If the file is a video, use its thumbnail, if available
|
||||||
|
var filePath = m.fileMetadata?.path;
|
||||||
|
var fileMime = m.fileMetadata?.mimeType;
|
||||||
|
|
||||||
|
// Thumbnail workaround for Android
|
||||||
|
if (Platform.isAndroid &&
|
||||||
|
(m.fileMetadata?.mimeType?.startsWith('video/') ?? false) &&
|
||||||
|
m.fileMetadata?.path != null) {
|
||||||
|
final thumbnailPath = await getVideoThumbnailPath(m.fileMetadata!.path!);
|
||||||
|
if (File(thumbnailPath).existsSync()) {
|
||||||
|
// Workaround for Android to show the thumbnail in the notification
|
||||||
|
filePath = thumbnailPath;
|
||||||
|
fileMime = 'image/jpeg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add to the database
|
// Add to the database
|
||||||
final newNotification = modeln.Notification(
|
final newNotification = modeln.Notification(
|
||||||
id,
|
id,
|
||||||
@ -230,8 +298,8 @@ class NotificationsService {
|
|||||||
senderJid.toString(),
|
senderJid.toString(),
|
||||||
(avatarPath?.isEmpty ?? false) ? null : avatarPath,
|
(avatarPath?.isEmpty ?? false) ? null : avatarPath,
|
||||||
body,
|
body,
|
||||||
m.fileMetadata?.mimeType,
|
fileMime,
|
||||||
m.fileMetadata?.path,
|
filePath,
|
||||||
m.timestamp,
|
m.timestamp,
|
||||||
);
|
);
|
||||||
await GetIt.I.get<DatabaseService>().database.insert(
|
await GetIt.I.get<DatabaseService>().database.insert(
|
||||||
@ -250,7 +318,7 @@ class NotificationsService {
|
|||||||
/// When a notification is already visible, then build a new notification based on [c] and [m],
|
/// When a notification is already visible, then build a new notification based on [c] and [m],
|
||||||
/// update the database state and tell the OS to show the notification again.
|
/// update the database state and tell the OS to show the notification again.
|
||||||
// TODO(Unknown): What about systems that cannot do this (Linux, OS X, Windows)?
|
// TODO(Unknown): What about systems that cannot do this (Linux, OS X, Windows)?
|
||||||
Future<void> updateNotification(
|
Future<void> updateOrShowNotification(
|
||||||
modelc.Conversation c,
|
modelc.Conversation c,
|
||||||
modelm.Message m,
|
modelm.Message m,
|
||||||
String accountJid,
|
String accountJid,
|
||||||
@ -263,37 +331,38 @@ class NotificationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final notifications = await _getNotificationsForJid(c.jid, accountJid);
|
final notifications = await _getNotificationsForJid(c.jid, accountJid);
|
||||||
final id = notifications.first.id;
|
final id = notifications.isNotEmpty
|
||||||
|
? notifications.first.id
|
||||||
|
: Random().nextInt(_maxNotificationId);
|
||||||
// TODO(Unknown): Handle groupchat member avatars
|
// TODO(Unknown): Handle groupchat member avatars
|
||||||
final notification = await _createNotification(
|
final notification = await _createNotification(
|
||||||
c,
|
c,
|
||||||
m,
|
m,
|
||||||
accountJid,
|
accountJid,
|
||||||
c.isGroupchat ? null : c.avatarPathWithOptionalContact,
|
c.isGroupchat ? null : await c.avatarPathWithOptionalContactService,
|
||||||
id,
|
id,
|
||||||
shouldOverride: true,
|
shouldOverride: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
await MoxplatformPlugin.notifications.showMessagingNotification(
|
await _api.showMessagingNotification(
|
||||||
MessagingNotification(
|
api.MessagingNotification(
|
||||||
title: await c.titleWithOptionalContactService,
|
title: await c.titleWithOptionalContactService,
|
||||||
id: id,
|
id: id,
|
||||||
channelId: _messageChannelKey,
|
channelId: messageNotificationChannelId,
|
||||||
jid: c.jid,
|
jid: c.jid,
|
||||||
messages: [
|
messages: notifications.map((n) {
|
||||||
...notifications.map((n) {
|
// Based on the table's composite primary key
|
||||||
// Based on the table's composite primary key
|
if (n.id == notification.id &&
|
||||||
if (n.id == notification.id &&
|
n.conversationJid == notification.conversationJid &&
|
||||||
n.conversationJid == notification.conversationJid &&
|
n.senderJid == notification.senderJid &&
|
||||||
n.senderJid == notification.senderJid &&
|
n.timestamp == notification.timestamp) {
|
||||||
n.timestamp == notification.timestamp) {
|
return notification.toNotificationMessage();
|
||||||
return notification.toNotificationMessage();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return n.toNotificationMessage();
|
return n.toNotificationMessage();
|
||||||
}),
|
}).toList(),
|
||||||
],
|
|
||||||
isGroupchat: c.isGroupchat,
|
isGroupchat: c.isGroupchat,
|
||||||
|
groupId: messageNotificationGroupId,
|
||||||
extra: {
|
extra: {
|
||||||
_conversationJidKey: c.jid,
|
_conversationJidKey: c.jid,
|
||||||
_messageIdKey: m.id,
|
_messageIdKey: m.id,
|
||||||
@ -325,11 +394,11 @@ class NotificationsService {
|
|||||||
final id = notifications.isNotEmpty
|
final id = notifications.isNotEmpty
|
||||||
? notifications.first.id
|
? notifications.first.id
|
||||||
: Random().nextInt(_maxNotificationId);
|
: Random().nextInt(_maxNotificationId);
|
||||||
await MoxplatformPlugin.notifications.showMessagingNotification(
|
await _api.showMessagingNotification(
|
||||||
MessagingNotification(
|
api.MessagingNotification(
|
||||||
title: title,
|
title: title,
|
||||||
id: id,
|
id: id,
|
||||||
channelId: _messageChannelKey,
|
channelId: messageNotificationChannelId,
|
||||||
jid: c.jid,
|
jid: c.jid,
|
||||||
messages: [
|
messages: [
|
||||||
...notifications.map((n) => n.toNotificationMessage()),
|
...notifications.map((n) => n.toNotificationMessage()),
|
||||||
@ -344,6 +413,7 @@ class NotificationsService {
|
|||||||
.toNotificationMessage(),
|
.toNotificationMessage(),
|
||||||
],
|
],
|
||||||
isGroupchat: c.isGroupchat,
|
isGroupchat: c.isGroupchat,
|
||||||
|
groupId: messageNotificationGroupId,
|
||||||
extra: {
|
extra: {
|
||||||
_conversationJidKey: c.jid,
|
_conversationJidKey: c.jid,
|
||||||
_messageIdKey: m.id,
|
_messageIdKey: m.id,
|
||||||
@ -364,13 +434,14 @@ class NotificationsService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await MoxplatformPlugin.notifications.showNotification(
|
await _api.showNotification(
|
||||||
RegularNotification(
|
api.RegularNotification(
|
||||||
title: title,
|
title: title,
|
||||||
body: body,
|
body: body,
|
||||||
channelId: _warningChannelKey,
|
channelId: warningNotificationChannelId,
|
||||||
id: Random().nextInt(_maxNotificationId),
|
id: Random().nextInt(_maxNotificationId),
|
||||||
icon: NotificationIcon.warning,
|
icon: api.NotificationIcon.warning,
|
||||||
|
groupId: warningNotificationGroupId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -401,14 +472,15 @@ class NotificationsService {
|
|||||||
final conversation = await GetIt.I
|
final conversation = await GetIt.I
|
||||||
.get<ConversationService>()
|
.get<ConversationService>()
|
||||||
.getConversationByJid(jid, accountJid);
|
.getConversationByJid(jid, accountJid);
|
||||||
await MoxplatformPlugin.notifications.showNotification(
|
await _api.showNotification(
|
||||||
RegularNotification(
|
api.RegularNotification(
|
||||||
title: t.notifications.errors.messageError.title,
|
title: t.notifications.errors.messageError.title,
|
||||||
body: t.notifications.errors.messageError
|
body: t.notifications.errors.messageError
|
||||||
.body(conversationTitle: conversation!.title),
|
.body(conversationTitle: conversation!.title),
|
||||||
channelId: _warningChannelKey,
|
channelId: warningNotificationChannelId,
|
||||||
id: Random().nextInt(_maxNotificationId),
|
id: Random().nextInt(_maxNotificationId),
|
||||||
icon: NotificationIcon.error,
|
icon: api.NotificationIcon.error,
|
||||||
|
groupId: warningNotificationGroupId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -418,7 +490,7 @@ class NotificationsService {
|
|||||||
Future<void> dismissNotificationsByJid(String jid, String accountJid) async {
|
Future<void> dismissNotificationsByJid(String jid, String accountJid) async {
|
||||||
final id = await _clearNotificationsForJid(jid, accountJid);
|
final id = await _clearNotificationsForJid(jid, accountJid);
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
await MoxplatformPlugin.notifications.dismissNotification(id);
|
await _api.dismissNotification(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,8 +507,7 @@ class NotificationsService {
|
|||||||
|
|
||||||
// Dismiss the notification
|
// Dismiss the notification
|
||||||
for (final idRaw in ids) {
|
for (final idRaw in ids) {
|
||||||
await MoxplatformPlugin.notifications
|
await _api.dismissNotification(idRaw['id']! as int);
|
||||||
.dismissNotification(idRaw['id']! as int);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove database entries
|
// Remove database entries
|
||||||
@ -450,11 +521,10 @@ class NotificationsService {
|
|||||||
/// Requests the avatar path from [XmppStateService] and configures the notification plugin
|
/// Requests the avatar path from [XmppStateService] and configures the notification plugin
|
||||||
/// accordingly, if the avatar path is not null. If it is null, this method does nothing.
|
/// accordingly, if the avatar path is not null. If it is null, this method does nothing.
|
||||||
Future<void> maybeSetAvatarFromState() async {
|
Future<void> maybeSetAvatarFromState() async {
|
||||||
final avatarPath =
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
(await GetIt.I.get<XmppStateService>().getXmppState()).avatarUrl;
|
final avatarPath = (await xss.state).avatarUrl;
|
||||||
if (avatarPath.isNotEmpty) {
|
if (avatarPath.isNotEmpty) {
|
||||||
await MoxplatformPlugin.notifications
|
await _api.setNotificationSelfAvatar(avatarPath);
|
||||||
.setNotificationSelfAvatar(avatarPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ class PermissionsService {
|
|||||||
/// `askedNotificationPermission` to true.
|
/// `askedNotificationPermission` to true.
|
||||||
Future<bool> shouldRequestNotificationPermission() async {
|
Future<bool> shouldRequestNotificationPermission() async {
|
||||||
final xss = GetIt.I.get<XmppStateService>();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final retValue = !(await xss.getXmppState()).askedNotificationPermission;
|
final retValue = !(await xss.state).askedNotificationPermission;
|
||||||
if (retValue) {
|
if (retValue) {
|
||||||
await xss.modifyXmppState(
|
await xss.modifyXmppState(
|
||||||
(state) => state.copyWith(askedNotificationPermission: true),
|
(state) => state.copyWith(askedNotificationPermission: true),
|
||||||
@ -29,8 +29,7 @@ class PermissionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final xss = GetIt.I.get<XmppStateService>();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final retValue =
|
final retValue = !(await xss.state).askedBatteryOptimizationExcemption;
|
||||||
!(await xss.getXmppState()).askedBatteryOptimizationExcemption;
|
|
||||||
if (retValue) {
|
if (retValue) {
|
||||||
await xss.modifyXmppState(
|
await xss.modifyXmppState(
|
||||||
(state) => state.copyWith(askedBatteryOptimizationExcemption: true),
|
(state) => state.copyWith(askedBatteryOptimizationExcemption: true),
|
||||||
|
@ -135,6 +135,7 @@ class ReactionsService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
await GetIt.I.get<DatabaseService>().database.delete(
|
await GetIt.I.get<DatabaseService>().database.delete(
|
||||||
reactionsTable,
|
reactionsTable,
|
||||||
where:
|
where:
|
||||||
@ -143,7 +144,7 @@ class ReactionsService {
|
|||||||
id,
|
id,
|
||||||
accountJid,
|
accountJid,
|
||||||
emoji,
|
emoji,
|
||||||
(await GetIt.I.get<XmppStateService>().getXmppState()).jid,
|
(await xss.state).jid,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final count = await _countReactions(id, accountJid, emoji);
|
final count = await _countReactions(id, accountJid, emoji);
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
import 'package:moxlib/moxlib.dart';
|
||||||
@ -16,16 +17,19 @@ import 'package:moxxyv2/service/contacts.dart';
|
|||||||
import 'package:moxxyv2/service/conversation.dart';
|
import 'package:moxxyv2/service/conversation.dart';
|
||||||
import 'package:moxxyv2/service/cryptography/cryptography.dart';
|
import 'package:moxxyv2/service/cryptography/cryptography.dart';
|
||||||
import 'package:moxxyv2/service/database/database.dart';
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
|
import 'package:moxxyv2/service/database/migration.dart';
|
||||||
import 'package:moxxyv2/service/events.dart';
|
import 'package:moxxyv2/service/events.dart';
|
||||||
import 'package:moxxyv2/service/files.dart';
|
import 'package:moxxyv2/service/files.dart';
|
||||||
import 'package:moxxyv2/service/groupchat.dart';
|
import 'package:moxxyv2/service/groupchat.dart';
|
||||||
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
||||||
import 'package:moxxyv2/service/language.dart';
|
import 'package:moxxyv2/service/language.dart';
|
||||||
|
import 'package:moxxyv2/service/lifecycle.dart';
|
||||||
import 'package:moxxyv2/service/message.dart';
|
import 'package:moxxyv2/service/message.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/connectivity.dart';
|
import 'package:moxxyv2/service/moxxmpp/connectivity.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/roster.dart';
|
import 'package:moxxyv2/service/moxxmpp/roster.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/socket.dart';
|
import 'package:moxxyv2/service/moxxmpp/socket.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/stream.dart';
|
import 'package:moxxyv2/service/moxxmpp/stream.dart';
|
||||||
|
import 'package:moxxyv2/service/non_database_migrations/0000_notification_channels.dart';
|
||||||
import 'package:moxxyv2/service/notifications.dart';
|
import 'package:moxxyv2/service/notifications.dart';
|
||||||
import 'package:moxxyv2/service/omemo/omemo.dart';
|
import 'package:moxxyv2/service/omemo/omemo.dart';
|
||||||
import 'package:moxxyv2/service/permissions.dart';
|
import 'package:moxxyv2/service/permissions.dart';
|
||||||
@ -70,10 +74,30 @@ Future<void> initializeServiceIfNeeded() async {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.info('Service is not running. Initializing service... ');
|
logger.info('Service is not running. Initializing service... ');
|
||||||
|
|
||||||
|
// Run non-db migrations
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
const versionKey = 'non_database_migrations_version';
|
||||||
|
final currentVersion = int.parse(
|
||||||
|
await storage.read(key: versionKey) ?? '0',
|
||||||
|
);
|
||||||
|
await runMigrations(
|
||||||
|
logger,
|
||||||
|
42,
|
||||||
|
const [
|
||||||
|
Migration(2, upgradeV1ToV2NonDb),
|
||||||
|
],
|
||||||
|
currentVersion,
|
||||||
|
'non-database',
|
||||||
|
commitVersion: (version) async =>
|
||||||
|
storage.write(key: versionKey, value: version.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
await handler.start(
|
await handler.start(
|
||||||
entrypoint,
|
entrypoint,
|
||||||
receiveUIEvent,
|
receiveUIEvent,
|
||||||
ui_events.handleIsolateEvent,
|
ui_events.handleIsolateEvent,
|
||||||
|
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,7 +169,7 @@ Future<void> initUDPLogger() async {
|
|||||||
|
|
||||||
/// The entrypoint for all platforms after the platform specific initilization is done.
|
/// The entrypoint for all platforms after the platform specific initilization is done.
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> entrypoint() async {
|
Future<void> entrypoint(String initialLocale) async {
|
||||||
setupLogging();
|
setupLogging();
|
||||||
setupBackgroundEventHandler();
|
setupBackgroundEventHandler();
|
||||||
|
|
||||||
@ -159,6 +183,9 @@ Future<void> entrypoint() async {
|
|||||||
GetIt.I.registerSingleton<DatabaseService>(DatabaseService());
|
GetIt.I.registerSingleton<DatabaseService>(DatabaseService());
|
||||||
await GetIt.I.get<DatabaseService>().initialize();
|
await GetIt.I.get<DatabaseService>().initialize();
|
||||||
|
|
||||||
|
// Initialize the account state
|
||||||
|
await GetIt.I.get<XmppStateService>().initializeXmppState();
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
GetIt.I.registerSingleton<ConnectivityWatcherService>(
|
GetIt.I.registerSingleton<ConnectivityWatcherService>(
|
||||||
ConnectivityWatcherService(),
|
ConnectivityWatcherService(),
|
||||||
@ -182,9 +209,27 @@ Future<void> entrypoint() async {
|
|||||||
GetIt.I.registerSingleton<StorageService>(StorageService());
|
GetIt.I.registerSingleton<StorageService>(StorageService());
|
||||||
GetIt.I.registerSingleton<ShareService>(ShareService());
|
GetIt.I.registerSingleton<ShareService>(ShareService());
|
||||||
GetIt.I.registerSingleton<PermissionsService>(PermissionsService());
|
GetIt.I.registerSingleton<PermissionsService>(PermissionsService());
|
||||||
|
GetIt.I.registerSingleton<LifecycleService>(LifecycleService());
|
||||||
final xmpp = XmppService();
|
final xmpp = XmppService();
|
||||||
GetIt.I.registerSingleton<XmppService>(xmpp);
|
GetIt.I.registerSingleton<XmppService>(xmpp);
|
||||||
|
|
||||||
|
// Set the locale before we initialize the notigications service to ensure
|
||||||
|
// the correct locale is used for the notification channels.
|
||||||
|
final preferredLocale =
|
||||||
|
(await GetIt.I.get<PreferencesService>().getPreferences())
|
||||||
|
.languageLocaleCode;
|
||||||
|
if (preferredLocale == 'default') {
|
||||||
|
LocaleSettings.setLocaleRaw(initialLocale);
|
||||||
|
GetIt.I.get<Logger>().finest(
|
||||||
|
'Setting locale to system locale ($initialLocale) per preferences',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
LocaleSettings.setLocaleRaw(preferredLocale);
|
||||||
|
GetIt.I.get<Logger>().finest(
|
||||||
|
'Setting locale to configured locale ($preferredLocale) per preferences',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await GetIt.I.get<NotificationsService>().initialize();
|
await GetIt.I.get<NotificationsService>().initialize();
|
||||||
await GetIt.I.get<ContactsService>().initialize();
|
await GetIt.I.get<ContactsService>().initialize();
|
||||||
await GetIt.I.get<ConnectivityService>().initialize();
|
await GetIt.I.get<ConnectivityService>().initialize();
|
||||||
@ -233,7 +278,7 @@ Future<void> entrypoint() async {
|
|||||||
(toJid, _) async =>
|
(toJid, _) async =>
|
||||||
GetIt.I.get<ConversationService>().shouldEncryptForConversation(
|
GetIt.I.get<ConversationService>().shouldEncryptForConversation(
|
||||||
toJid,
|
toJid,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PingManager(const Duration(minutes: 3)),
|
PingManager(const Duration(minutes: 3)),
|
||||||
|
@ -155,7 +155,8 @@ JOIN
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Retract from PubSub
|
// Retract from PubSub
|
||||||
final state = await GetIt.I.get<XmppStateService>().getXmppState();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
|
final state = await xss.state;
|
||||||
final result = await GetIt.I
|
final result = await GetIt.I
|
||||||
.get<moxxmpp.XmppConnection>()
|
.get<moxxmpp.XmppConnection>()
|
||||||
.getManagerById<moxxmpp.StickersManager>(moxxmpp.stickersManager)!
|
.getManagerById<moxxmpp.StickersManager>(moxxmpp.stickersManager)!
|
||||||
@ -168,7 +169,8 @@ JOIN
|
|||||||
|
|
||||||
Future<void> _publishStickerPack(moxxmpp.StickerPack pack) async {
|
Future<void> _publishStickerPack(moxxmpp.StickerPack pack) async {
|
||||||
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
final state = await GetIt.I.get<XmppStateService>().getXmppState();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
|
final state = await xss.state;
|
||||||
final result = await GetIt.I
|
final result = await GetIt.I
|
||||||
.get<moxxmpp.XmppConnection>()
|
.get<moxxmpp.XmppConnection>()
|
||||||
.getManagerById<moxxmpp.StickersManager>(moxxmpp.stickersManager)!
|
.getManagerById<moxxmpp.StickersManager>(moxxmpp.stickersManager)!
|
||||||
|
@ -23,6 +23,7 @@ import 'package:moxxyv2/service/httpfiletransfer/helpers.dart';
|
|||||||
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
||||||
import 'package:moxxyv2/service/httpfiletransfer/jobs.dart';
|
import 'package:moxxyv2/service/httpfiletransfer/jobs.dart';
|
||||||
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
||||||
|
import 'package:moxxyv2/service/lifecycle.dart';
|
||||||
import 'package:moxxyv2/service/message.dart';
|
import 'package:moxxyv2/service/message.dart';
|
||||||
import 'package:moxxyv2/service/not_specified.dart';
|
import 'package:moxxyv2/service/not_specified.dart';
|
||||||
import 'package:moxxyv2/service/notifications.dart';
|
import 'package:moxxyv2/service/notifications.dart';
|
||||||
@ -81,22 +82,15 @@ class XmppService {
|
|||||||
/// Flag indicating whether a login was triggered from the UI or not.
|
/// Flag indicating whether a login was triggered from the UI or not.
|
||||||
bool _loginTriggeredFromUI = false;
|
bool _loginTriggeredFromUI = false;
|
||||||
|
|
||||||
/// Flag indicating whether the app is currently open or not.
|
|
||||||
bool _appOpen = true;
|
|
||||||
|
|
||||||
/// The JID of the currently opened chat. Empty, if no chat is opened.
|
|
||||||
String _currentlyOpenedChatJid = '';
|
|
||||||
|
|
||||||
/// Subscription to events by the XmppConnection
|
/// Subscription to events by the XmppConnection
|
||||||
StreamSubscription<dynamic>? _xmppConnectionSubscription;
|
StreamSubscription<dynamic>? _xmppConnectionSubscription;
|
||||||
|
|
||||||
/// Stores whether the app is open or not. Useful for notifications.
|
|
||||||
void setAppState(bool open) {
|
|
||||||
_appOpen = open;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ConnectionSettings?> getConnectionSettings() async {
|
Future<ConnectionSettings?> getConnectionSettings() async {
|
||||||
final state = await GetIt.I.get<XmppStateService>().getXmppState();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
|
final accountJid = await xss.getAccountJid();
|
||||||
|
if (accountJid == null) return null;
|
||||||
|
|
||||||
|
final state = await GetIt.I.get<XmppStateService>().state;
|
||||||
|
|
||||||
if (state.jid == null || state.password == null) {
|
if (state.jid == null || state.password == null) {
|
||||||
return null;
|
return null;
|
||||||
@ -112,13 +106,11 @@ class XmppService {
|
|||||||
/// greater than 0.
|
/// greater than 0.
|
||||||
Future<void> setCurrentlyOpenedChatJid(String jid) async {
|
Future<void> setCurrentlyOpenedChatJid(String jid) async {
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>()..activeConversationJid = jid;
|
||||||
|
|
||||||
_currentlyOpenedChatJid = jid;
|
|
||||||
|
|
||||||
final conversation = await cs.createOrUpdateConversation(
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
jid,
|
jid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
update: (c) async {
|
update: (c) async {
|
||||||
if (c.unreadCounter > 0) {
|
if (c.unreadCounter > 0) {
|
||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
@ -139,9 +131,6 @@ class XmppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the JID of the chat that is currently opened. Null, if none is open.
|
|
||||||
String? getCurrentlyOpenedChatJid() => _currentlyOpenedChatJid;
|
|
||||||
|
|
||||||
/// Sends a message correction to [recipient] regarding the message with stanza id
|
/// Sends a message correction to [recipient] regarding the message with stanza id
|
||||||
/// [oldId]. The old message's body gets corrected to [newBody]. [id] is the message's
|
/// [oldId]. The old message's body gets corrected to [newBody]. [id] is the message's
|
||||||
/// id. [chatState] can be optionally specified to also include a chat state
|
/// id. [chatState] can be optionally specified to also include a chat state
|
||||||
@ -475,7 +464,7 @@ class XmppService {
|
|||||||
bool triggeredFromUI,
|
bool triggeredFromUI,
|
||||||
) async {
|
) async {
|
||||||
final xss = GetIt.I.get<XmppStateService>();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final state = await xss.getXmppState();
|
final state = await xss.state;
|
||||||
final conn = GetIt.I.get<XmppConnection>();
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
final lastResource = state.resource ?? '';
|
final lastResource = state.resource ?? '';
|
||||||
|
|
||||||
@ -503,8 +492,8 @@ class XmppService {
|
|||||||
bool triggeredFromUI,
|
bool triggeredFromUI,
|
||||||
) async {
|
) async {
|
||||||
final xss = GetIt.I.get<XmppStateService>();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final state = await xss.getXmppState();
|
|
||||||
final conn = GetIt.I.get<XmppConnection>();
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
final state = await xss.state;
|
||||||
final lastResource = state.resource ?? '';
|
final lastResource = state.resource ?? '';
|
||||||
|
|
||||||
_loginTriggeredFromUI = triggeredFromUI;
|
_loginTriggeredFromUI = triggeredFromUI;
|
||||||
@ -859,7 +848,7 @@ class XmppService {
|
|||||||
.requestRoster();
|
.requestRoster();
|
||||||
|
|
||||||
await GetIt.I.get<BlocklistService>().getBlocklist(
|
await GetIt.I.get<BlocklistService>().getBlocklist(
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -913,7 +902,7 @@ class XmppService {
|
|||||||
final rs = GetIt.I.get<RosterService>();
|
final rs = GetIt.I.get<RosterService>();
|
||||||
final rosterItem = await rs.getRosterItemByJid(
|
final rosterItem = await rs.getRosterItemByJid(
|
||||||
jid.toString(),
|
jid.toString(),
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
if (rosterItem != null) {
|
if (rosterItem != null) {
|
||||||
final pm = GetIt.I
|
final pm = GetIt.I
|
||||||
@ -941,7 +930,7 @@ class XmppService {
|
|||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final dbMsg = await ms.getMessageByStanzaId(
|
final dbMsg = await ms.getMessageByStanzaId(
|
||||||
event.id,
|
event.id,
|
||||||
accountJid,
|
accountJid!,
|
||||||
queryReactionPreview: false,
|
queryReactionPreview: false,
|
||||||
);
|
);
|
||||||
if (dbMsg == null) {
|
if (dbMsg == null) {
|
||||||
@ -976,7 +965,7 @@ class XmppService {
|
|||||||
final sender = event.from.toBare().toString();
|
final sender = event.from.toBare().toString();
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
// TODO(Unknown): With groupchats, we should use the groupchat assigned stanza-id
|
// TODO(Unknown): With groupchats, we should use the groupchat assigned stanza-id
|
||||||
final dbMsg = await ms.getMessageByStanzaId(event.id, accountJid);
|
final dbMsg = await ms.getMessageByStanzaId(event.id, accountJid!);
|
||||||
if (dbMsg == null) {
|
if (dbMsg == null) {
|
||||||
_log.warning('Did not find the message in the database!');
|
_log.warning('Did not find the message in the database!');
|
||||||
return;
|
return;
|
||||||
@ -1009,7 +998,7 @@ class XmppService {
|
|||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
final conversation = await cs.getConversationByJid(
|
final conversation = await cs.getConversationByJid(
|
||||||
jid,
|
jid,
|
||||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
);
|
);
|
||||||
if (conversation == null) return;
|
if (conversation == null) return;
|
||||||
|
|
||||||
@ -1281,7 +1270,7 @@ class XmppService {
|
|||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
|
|
||||||
if (event.type == 'error') {
|
if (event.type == 'error') {
|
||||||
await _handleErrorMessage(event, accountJid);
|
await _handleErrorMessage(event, accountJid!);
|
||||||
_log.finest('Processed error message. Ending event processing here.');
|
_log.finest('Processed error message. Ending event processing here.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1294,7 +1283,7 @@ class XmppService {
|
|||||||
|
|
||||||
// Process message corrections separately
|
// Process message corrections separately
|
||||||
if (event.extensions.get<LastMessageCorrectionData>() != null) {
|
if (event.extensions.get<LastMessageCorrectionData>() != null) {
|
||||||
await _handleMessageCorrection(event, conversationJid, accountJid);
|
await _handleMessageCorrection(event, conversationJid, accountJid!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1303,19 +1292,19 @@ class XmppService {
|
|||||||
await _handleFileUploadNotificationReplacement(
|
await _handleFileUploadNotificationReplacement(
|
||||||
event,
|
event,
|
||||||
conversationJid,
|
conversationJid,
|
||||||
accountJid,
|
accountJid!,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.extensions.get<MessageRetractionData>() != null) {
|
if (event.extensions.get<MessageRetractionData>() != null) {
|
||||||
await _handleMessageRetraction(event, conversationJid, accountJid);
|
await _handleMessageRetraction(event, conversationJid, accountJid!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle message reactions
|
// Handle message reactions
|
||||||
if (event.extensions.get<MessageReactionsData>() != null) {
|
if (event.extensions.get<MessageReactionsData>() != null) {
|
||||||
await _handleMessageReactions(event, conversationJid, accountJid);
|
await _handleMessageReactions(event, conversationJid, accountJid!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1332,12 +1321,12 @@ class XmppService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final state = await GetIt.I.get<XmppStateService>().getXmppState();
|
final state = await GetIt.I.get<XmppStateService>().state;
|
||||||
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
// The (portential) roster item of the chat partner
|
// The (portential) roster item of the chat partner
|
||||||
final rosterItem = await GetIt.I
|
final rosterItem = await GetIt.I
|
||||||
.get<RosterService>()
|
.get<RosterService>()
|
||||||
.getRosterItemByJid(conversationJid, accountJid);
|
.getRosterItemByJid(conversationJid, accountJid!);
|
||||||
// Is the conversation partner in our roster
|
// Is the conversation partner in our roster
|
||||||
final isInRoster = rosterItem != null;
|
final isInRoster = rosterItem != null;
|
||||||
// True if the message was sent by us (via a Carbon)
|
// True if the message was sent by us (via a Carbon)
|
||||||
@ -1524,7 +1513,7 @@ class XmppService {
|
|||||||
? mimeTypeToEmoji(mimeGuess)
|
? mimeTypeToEmoji(mimeGuess)
|
||||||
: messageBody;
|
: messageBody;
|
||||||
// Specifies if we have the conversation this message goes to opened
|
// Specifies if we have the conversation this message goes to opened
|
||||||
final isConversationOpened = _currentlyOpenedChatJid == conversationJid;
|
final isConversationOpened = cs.activeConversationJid == conversationJid;
|
||||||
// If the conversation is muted
|
// If the conversation is muted
|
||||||
var isMuted = false;
|
var isMuted = false;
|
||||||
// Whether to send the notification
|
// Whether to send the notification
|
||||||
@ -1588,7 +1577,8 @@ class XmppService {
|
|||||||
isMuted = c != null ? c.muted : prefs.defaultMuteState;
|
isMuted = c != null ? c.muted : prefs.defaultMuteState;
|
||||||
sendNotification = !sent &&
|
sendNotification = !sent &&
|
||||||
shouldNotify &&
|
shouldNotify &&
|
||||||
(!isConversationOpened || !_appOpen) &&
|
(!isConversationOpened ||
|
||||||
|
!GetIt.I.get<LifecycleService>().isActive) &&
|
||||||
!isMuted;
|
!isMuted;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -1730,7 +1720,7 @@ class XmppService {
|
|||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final msg = await ms.getMessageByStanzaId(event.stanza.id!, accountJid);
|
final msg = await ms.getMessageByStanzaId(event.stanza.id!, accountJid!);
|
||||||
if (msg != null) {
|
if (msg != null) {
|
||||||
// Ack the message
|
// Ack the message
|
||||||
final newMsg = await ms.updateMessage(
|
final newMsg = await ms.updateMessage(
|
||||||
@ -1790,7 +1780,7 @@ class XmppService {
|
|||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
final message = await ms.getMessageByStanzaId(
|
final message = await ms.getMessageByStanzaId(
|
||||||
event.data.stanza.id!,
|
event.data.stanza.id!,
|
||||||
accountJid,
|
accountJid!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
|
@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
import 'package:moxxyv2/service/database/database.dart';
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
|
import 'package:moxxyv2/service/xmpp.dart';
|
||||||
import 'package:moxxyv2/shared/models/xmpp_state.dart';
|
import 'package:moxxyv2/shared/models/xmpp_state.dart';
|
||||||
import 'package:random_string/random_string.dart';
|
import 'package:random_string/random_string.dart';
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
@ -30,7 +31,9 @@ class XmppStateService {
|
|||||||
final Logger _log = Logger('XmppStateService');
|
final Logger _log = Logger('XmppStateService');
|
||||||
|
|
||||||
/// Persistent state around the connection, like the SM token, etc.
|
/// Persistent state around the connection, like the SM token, etc.
|
||||||
XmppState? _state;
|
late XmppState _state;
|
||||||
|
final Lock _stateLock = Lock();
|
||||||
|
Future<XmppState> get state => _stateLock.synchronized(() => _state);
|
||||||
|
|
||||||
/// Cached account JID.
|
/// Cached account JID.
|
||||||
String? _accountJid;
|
String? _accountJid;
|
||||||
@ -88,6 +91,7 @@ class XmppStateService {
|
|||||||
await db.insert(
|
await db.insert(
|
||||||
xmppStateTable,
|
xmppStateTable,
|
||||||
{
|
{
|
||||||
|
'accountJid': _accountJid,
|
||||||
'key': _userAgentKey,
|
'key': _userAgentKey,
|
||||||
'value': jsonEncode(_userAgent!.toJson()),
|
'value': jsonEncode(_userAgent!.toJson()),
|
||||||
},
|
},
|
||||||
@ -111,32 +115,48 @@ class XmppStateService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<XmppState> getXmppState() async {
|
Future<void> initializeXmppState() async {
|
||||||
if (_state != null) return _state!;
|
// NOTE: Called only once at the start so we don't have to worry about aquiring a lock
|
||||||
|
await _loadAccountJid();
|
||||||
|
final state = await _loadXmppState(_accountJid);
|
||||||
|
if (_accountJid == null || state == null) {
|
||||||
|
_log.finest(
|
||||||
|
'No account JID or account state available. Creating default value',
|
||||||
|
);
|
||||||
|
_state = XmppState(jid: _accountJid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<XmppState?> _loadXmppState(String? accountJid) async {
|
||||||
|
if (accountJid == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final json = <String, String?>{};
|
final json = <String, String?>{};
|
||||||
final rowsRaw = await GetIt.I.get<DatabaseService>().database.query(
|
final rowsRaw = await GetIt.I.get<DatabaseService>().database.query(
|
||||||
xmppStateTable,
|
xmppStateTable,
|
||||||
where: 'accountJid = ?',
|
where: 'accountJid = ?',
|
||||||
whereArgs: [await getAccountJid()],
|
whereArgs: [accountJid],
|
||||||
columns: ['key', 'value'],
|
columns: ['key', 'value'],
|
||||||
);
|
);
|
||||||
|
if (rowsRaw.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (final row in rowsRaw) {
|
for (final row in rowsRaw) {
|
||||||
json[row['key']! as String] = row['value'] as String?;
|
json[row['key']! as String] = row['value'] as String?;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.finest(json);
|
return XmppState.fromDatabaseTuples(json);
|
||||||
_state = XmppState.fromDatabaseTuples(json);
|
|
||||||
return _state!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper to modify the [XmppState] and commit it.
|
/// The same as [commitXmppState] but without aquiring [_stateLock].
|
||||||
Future<void> modifyXmppState(XmppState Function(XmppState) func) async {
|
Future<void> _commitXmppState(String accountJid) async {
|
||||||
_state = func(_state!);
|
|
||||||
|
|
||||||
final accountJid = await getAccountJid();
|
|
||||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
||||||
for (final tuple in _state!.toDatabaseTuples().entries) {
|
for (final tuple in _state.toDatabaseTuples().entries) {
|
||||||
batch.insert(
|
batch.insert(
|
||||||
xmppStateTable,
|
xmppStateTable,
|
||||||
<String, String?>{
|
<String, String?>{
|
||||||
@ -150,6 +170,43 @@ class XmppStateService {
|
|||||||
await batch.commit();
|
await batch.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> commitXmppState(String accountJid) async {
|
||||||
|
await _stateLock.synchronized(
|
||||||
|
() => _commitXmppState(accountJid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setXmppState(XmppState state, String accountJid) async {
|
||||||
|
await _stateLock.synchronized(
|
||||||
|
() async {
|
||||||
|
_state = state;
|
||||||
|
await _commitXmppState(accountJid);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper to modify the [XmppState] and commit it.
|
||||||
|
Future<void> modifyXmppState(
|
||||||
|
XmppState Function(XmppState) func, {
|
||||||
|
bool commit = true,
|
||||||
|
}) async {
|
||||||
|
final accountJid = await getAccountJid();
|
||||||
|
assert(
|
||||||
|
accountJid != null,
|
||||||
|
'The accountJid must be not empty',
|
||||||
|
);
|
||||||
|
|
||||||
|
await _stateLock.synchronized(
|
||||||
|
() async {
|
||||||
|
_state = func(_state);
|
||||||
|
|
||||||
|
if (commit) {
|
||||||
|
await _commitXmppState(accountJid!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Resets the current account JID to null.
|
/// Resets the current account JID to null.
|
||||||
Future<void> resetAccountJid() async {
|
Future<void> resetAccountJid() async {
|
||||||
_accountJid = null;
|
_accountJid = null;
|
||||||
@ -157,26 +214,29 @@ class XmppStateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the current account JID to [jid] and stores it in the secure storage.
|
/// Sets the current account JID to [jid] and stores it in the secure storage.
|
||||||
Future<void> setAccountJid(String jid) async {
|
Future<void> setAccountJid(String jid, {bool commit = true}) async {
|
||||||
_accountJid = jid;
|
_accountJid = jid;
|
||||||
await _storage.write(key: _accountJidKey, value: jid);
|
|
||||||
|
if (commit) {
|
||||||
|
await _storage.write(key: _accountJidKey, value: jid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _loadAccountJid() async {
|
Future<String?> _loadAccountJid() async {
|
||||||
return _accountJid ??= await _storage.read(key: _accountJidKey);
|
return _accountJid ??= await _storage.read(key: _accountJidKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string if we have an account jid and null if we don't.
|
/// Gets the current account JID from the cache or from the secure storage.
|
||||||
Future<String?> getRawAccountJid() async {
|
Future<String?> getAccountJid() async {
|
||||||
if (_accountJid != null) {
|
return _accountJid ?? await _loadAccountJid();
|
||||||
return _accountJid;
|
}
|
||||||
|
|
||||||
|
Future<bool> isLoggedIn(String? accountJid) async {
|
||||||
|
final s = await state;
|
||||||
|
if (accountJid == null || s.jid == null || s.password == null) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _loadAccountJid();
|
return await GetIt.I.get<XmppService>().getConnectionSettings() != null;
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the current account JID from the cache or from the secure storage.
|
|
||||||
Future<String> getAccountJid() async {
|
|
||||||
return _accountJid ?? (await _loadAccountJid())!;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,4 +23,14 @@ const maxStickerPackPages = 2;
|
|||||||
|
|
||||||
/// An "invalid" fake JID to make share_handler happy when adding the self-chat
|
/// An "invalid" fake JID to make share_handler happy when adding the self-chat
|
||||||
/// to the direct share list.
|
/// to the direct share list.
|
||||||
const String selfChatShareFakeJid = '{{ self-chat }}';
|
const selfChatShareFakeJid = '{{ self-chat }}';
|
||||||
|
|
||||||
|
/// Keys for grouping notifications
|
||||||
|
const messageNotificationGroupId = 'message';
|
||||||
|
const warningNotificationGroupId = 'warning';
|
||||||
|
const foregroundServiceNotificationGroupId = 'service';
|
||||||
|
|
||||||
|
/// Notification channel ids
|
||||||
|
const foregroundServiceNotificationChannelId = 'foreground_service';
|
||||||
|
const messageNotificationChannelId = 'messages';
|
||||||
|
const warningNotificationChannelId = 'warnings';
|
||||||
|
@ -8,7 +8,6 @@ import 'package:moxxyv2/shared/models/message.dart';
|
|||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
|
||||||
|
|
||||||
/// Add a leading zero, if required, to ensure that an integer is rendered
|
/// Add a leading zero, if required, to ensure that an integer is rendered
|
||||||
/// as a two "digit" string.
|
/// as a two "digit" string.
|
||||||
@ -366,49 +365,13 @@ Future<Size?> getImageSizeFromData(Uint8List bytes) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a thumbnail file (JPEG) for the video at [path]. [conversationJid] refers
|
/// Returns true if we can generate a video thumbnail of mime type [mime]. If not, returns
|
||||||
/// to the JID of the conversation the file comes from.
|
/// false.
|
||||||
/// If the thumbnail already exists, then just its path is returned. If not, then
|
bool canGenerateVideoThumbnail(String mime) {
|
||||||
/// it gets generated first.
|
return ![
|
||||||
Future<String?> getVideoThumbnailPath(
|
// Ignore mime types that may be wacky
|
||||||
String path,
|
'video/webm',
|
||||||
String conversationJid,
|
].contains(mime);
|
||||||
String mime,
|
|
||||||
) async {
|
|
||||||
//print('getVideoThumbnailPath: Mime type: $mime');
|
|
||||||
|
|
||||||
// Ignore mime types that may be wacky
|
|
||||||
if (mime == 'video/webm') return null;
|
|
||||||
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
final thumbnailFilenameNoExtension = p.withoutExtension(
|
|
||||||
p.basename(path),
|
|
||||||
);
|
|
||||||
final thumbnailFilename = '$thumbnailFilenameNoExtension.jpg';
|
|
||||||
final thumbnailDirectory = p.join(
|
|
||||||
tempDir.path,
|
|
||||||
'thumbnails',
|
|
||||||
conversationJid,
|
|
||||||
);
|
|
||||||
final thumbnailPath = p.join(thumbnailDirectory, thumbnailFilename);
|
|
||||||
|
|
||||||
final dir = Directory(thumbnailDirectory);
|
|
||||||
if (!dir.existsSync()) await dir.create(recursive: true);
|
|
||||||
final file = File(thumbnailPath);
|
|
||||||
if (file.existsSync()) return thumbnailPath;
|
|
||||||
|
|
||||||
final r = await VideoThumbnail.thumbnailFile(
|
|
||||||
video: path,
|
|
||||||
thumbnailPath: thumbnailDirectory,
|
|
||||||
imageFormat: ImageFormat.JPEG,
|
|
||||||
quality: 75,
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
r == thumbnailPath,
|
|
||||||
'The generated video thumbnail has a different path than we expected: $r vs. $thumbnailPath',
|
|
||||||
);
|
|
||||||
|
|
||||||
return thumbnailPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getContactProfilePicturePath(String id) async {
|
Future<String> getContactProfilePicturePath(String id) async {
|
||||||
|
@ -62,6 +62,23 @@ enum ConversationType {
|
|||||||
throw Exception();
|
throw Exception();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the "type" attribute value for a message within the enum value's
|
||||||
|
/// context.
|
||||||
|
String toMessageType() {
|
||||||
|
assert(
|
||||||
|
this != ConversationType.note,
|
||||||
|
'Chat states should not be sent to the self-chat',
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (this) {
|
||||||
|
case ConversationType.note:
|
||||||
|
case ConversationType.chat:
|
||||||
|
return 'chat';
|
||||||
|
case ConversationType.groupchat:
|
||||||
|
return 'groupchat';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConversationTypeConverter
|
class ConversationTypeConverter
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxxyv2/service/pigeon/api.g.dart' as api;
|
||||||
|
|
||||||
part 'notification.freezed.dart';
|
part 'notification.freezed.dart';
|
||||||
part 'notification.g.dart';
|
part 'notification.g.dart';
|
||||||
@ -44,12 +44,12 @@ class Notification with _$Notification {
|
|||||||
factory Notification.fromJson(Map<String, dynamic> json) =>
|
factory Notification.fromJson(Map<String, dynamic> json) =>
|
||||||
_$NotificationFromJson(json);
|
_$NotificationFromJson(json);
|
||||||
|
|
||||||
NotificationMessage toNotificationMessage() {
|
api.NotificationMessage toNotificationMessage() {
|
||||||
return NotificationMessage(
|
return api.NotificationMessage(
|
||||||
sender: sender,
|
sender: sender,
|
||||||
jid: senderJid,
|
jid: senderJid,
|
||||||
avatarPath: avatarPath,
|
avatarPath: avatarPath,
|
||||||
content: NotificationMessageContent(
|
content: api.NotificationMessageContent(
|
||||||
body: body,
|
body: body,
|
||||||
mime: mime,
|
mime: mime,
|
||||||
path: path,
|
path: path,
|
||||||
|
@ -97,4 +97,6 @@ class XmppState with _$XmppState {
|
|||||||
askedBatteryOptimizationExcemption ? 'true' : 'false',
|
askedBatteryOptimizationExcemption ? 'true' : 'false',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get canLogIn => jid != null && password != null;
|
||||||
}
|
}
|
||||||
|
41
lib/shared/thumbnails/helpers.dart
Normal file
41
lib/shared/thumbnails/helpers.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
Future<String> getVideoThumbnailPath(String path) async {
|
||||||
|
final tempDir = await MoxplatformPlugin.platform.getCacheDataPath();
|
||||||
|
final thumbnailFilenameNoExtension = p.withoutExtension(
|
||||||
|
p.basename(path),
|
||||||
|
);
|
||||||
|
final thumbnailFilename = '$thumbnailFilenameNoExtension.jpg';
|
||||||
|
final thumbnailDirectory = p.join(
|
||||||
|
tempDir,
|
||||||
|
'thumbnails',
|
||||||
|
);
|
||||||
|
return p.join(thumbnailDirectory, thumbnailFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a thumbnail file (JPEG) for the video at [path].
|
||||||
|
/// If the thumbnail already exists, then just its path is returned. If not, then
|
||||||
|
/// it gets generated first.
|
||||||
|
Future<String?> maybeGenerateVideoThumbnail(
|
||||||
|
String path,
|
||||||
|
) async {
|
||||||
|
final thumbnailPath = await getVideoThumbnailPath(path);
|
||||||
|
final thumbnailDirectory = p.dirname(thumbnailPath);
|
||||||
|
final dir = Directory(thumbnailDirectory);
|
||||||
|
if (!dir.existsSync()) await dir.create(recursive: true);
|
||||||
|
final file = File(thumbnailPath);
|
||||||
|
if (file.existsSync()) return thumbnailPath;
|
||||||
|
|
||||||
|
final success = await MoxplatformPlugin.platform
|
||||||
|
.generateVideoThumbnail(path, thumbnailPath, 720);
|
||||||
|
if (!success) {
|
||||||
|
GetIt.I.get<Logger>().warning('Failed to generate thumbnail for $path');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnailPath;
|
||||||
|
}
|
@ -22,6 +22,7 @@ class ConversationsBloc extends Bloc<ConversationsEvent, ConversationsState> {
|
|||||||
on<ConversationClosedEvent>(_onConversationClosed);
|
on<ConversationClosedEvent>(_onConversationClosed);
|
||||||
on<ConversationMarkedAsReadEvent>(_onConversationMarkedAsRead);
|
on<ConversationMarkedAsReadEvent>(_onConversationMarkedAsRead);
|
||||||
on<ConversationsSetEvent>(_onConversationsSet);
|
on<ConversationsSetEvent>(_onConversationsSet);
|
||||||
|
on<ConversationExitedEvent>(_onConversationExited);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(Unknown): This pattern is used so often that it should become its own thing in moxlib
|
// TODO(Unknown): This pattern is used so often that it should become its own thing in moxlib
|
||||||
@ -70,6 +71,18 @@ class ConversationsBloc extends Bloc<ConversationsEvent, ConversationsState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onConversationExited(
|
||||||
|
ConversationExitedEvent event,
|
||||||
|
Emitter<ConversationsState> emit,
|
||||||
|
) async {
|
||||||
|
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
|
ExitConversationCommand(
|
||||||
|
conversationType: event.type.toString(),
|
||||||
|
),
|
||||||
|
awaitable: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onConversationsAdded(
|
Future<void> _onConversationsAdded(
|
||||||
ConversationsAddedEvent event,
|
ConversationsAddedEvent event,
|
||||||
Emitter<ConversationsState> emit,
|
Emitter<ConversationsState> emit,
|
||||||
|
@ -16,6 +16,14 @@ class ConversationsInitEvent extends ConversationsEvent {
|
|||||||
final List<Conversation> conversations;
|
final List<Conversation> conversations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Triggered when a conversation has been exited
|
||||||
|
class ConversationExitedEvent extends ConversationsEvent {
|
||||||
|
ConversationExitedEvent(this.type);
|
||||||
|
|
||||||
|
/// The type of the conversation that we just exited.
|
||||||
|
final ConversationType type;
|
||||||
|
}
|
||||||
|
|
||||||
/// Triggered when a conversation has been added.
|
/// Triggered when a conversation has been added.
|
||||||
class ConversationsAddedEvent extends ConversationsEvent {
|
class ConversationsAddedEvent extends ConversationsEvent {
|
||||||
ConversationsAddedEvent(this.conversation);
|
ConversationsAddedEvent(this.conversation);
|
||||||
|
@ -7,6 +7,7 @@ import 'package:moxxyv2/shared/events.dart';
|
|||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/request_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
import 'package:moxxyv2/ui/service/data.dart';
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
|
|
||||||
@ -106,6 +107,16 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
|
|||||||
(_) => false,
|
(_) => false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
GetIt.I.get<RequestBloc>().add(
|
||||||
|
RequestsSetEvent(
|
||||||
|
[
|
||||||
|
if (result.preStart.requestNotificationPermission)
|
||||||
|
Request.notifications,
|
||||||
|
if (result.preStart.excludeFromBatteryOptimisation)
|
||||||
|
Request.batterySavingExcemption,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (result is LoginFailureEvent) {
|
} else if (result is LoginFailureEvent) {
|
||||||
GetIt.I.get<UIDataService>().isLoggedIn = false;
|
GetIt.I.get<UIDataService>().isLoggedIn = false;
|
||||||
return emit(
|
return emit(
|
||||||
|
@ -568,9 +568,6 @@ class BidirectionalConversationController
|
|||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
_audioRecorder.dispose();
|
_audioRecorder.dispose();
|
||||||
|
|
||||||
// Tell the contact that we're gone
|
|
||||||
_updateChatState(ChatState.gone);
|
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import 'package:moxxyv2/shared/helpers.dart';
|
|||||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||||
import 'package:moxxyv2/shared/models/message.dart';
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
||||||
import 'package:moxxyv2/ui/helpers.dart';
|
import 'package:moxxyv2/ui/helpers.dart';
|
||||||
import 'package:moxxyv2/ui/pages/conversation/blink.dart';
|
import 'package:moxxyv2/ui/pages/conversation/blink.dart';
|
||||||
@ -373,6 +374,13 @@ class ConversationPageState extends State<ConversationPage>
|
|||||||
|
|
||||||
// Clear the read marker cache
|
// Clear the read marker cache
|
||||||
GetIt.I.get<UIReadMarkerService>().clear();
|
GetIt.I.get<UIReadMarkerService>().clear();
|
||||||
|
|
||||||
|
// Tell the backend that the chat is no longer open
|
||||||
|
GetIt.I.get<ConversationsBloc>().add(
|
||||||
|
ConversationExitedEvent(
|
||||||
|
ConversationType.fromString(widget.conversationType),
|
||||||
|
),
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
child: KeyboardReplacerScaffold(
|
child: KeyboardReplacerScaffold(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/request_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/request_bloc.dart';
|
||||||
@ -39,18 +40,18 @@ class RequestDialog extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
switch (request) {
|
switch (request) {
|
||||||
case Request.notifications:
|
case Request.notifications:
|
||||||
Permission.notification.request();
|
await Permission.notification.request();
|
||||||
break;
|
break;
|
||||||
case Request.batterySavingExcemption:
|
case Request.batterySavingExcemption:
|
||||||
MoxplatformPlugin.platform
|
await MoxplatformPlugin.platform
|
||||||
.openBatteryOptimisationSettings();
|
.openBatteryOptimisationSettings();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.read<RequestBloc>().add(NextRequestEvent());
|
GetIt.I.get<RequestBloc>().add(NextRequestEvent());
|
||||||
},
|
},
|
||||||
child: Text(t.permissions.allow),
|
child: Text(t.permissions.allow),
|
||||||
),
|
),
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
|
import 'package:moxxyv2/shared/thumbnails/helpers.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/shimmer.dart';
|
import 'package:moxxyv2/ui/widgets/shimmer.dart';
|
||||||
|
|
||||||
|
Future<String?> _videoThumbnailWrapper(String path, String mime) async {
|
||||||
|
if (!canGenerateVideoThumbnail(mime)) return null;
|
||||||
|
|
||||||
|
return maybeGenerateVideoThumbnail(path);
|
||||||
|
}
|
||||||
|
|
||||||
class VideoThumbnail extends StatelessWidget {
|
class VideoThumbnail extends StatelessWidget {
|
||||||
const VideoThumbnail({
|
const VideoThumbnail({
|
||||||
required this.path,
|
required this.path,
|
||||||
@ -21,7 +28,7 @@ class VideoThumbnail extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<String?>(
|
return FutureBuilder<String?>(
|
||||||
future: getVideoThumbnailPath(path, conversationJid, mime),
|
future: _videoThumbnailWrapper(path, mime),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
Widget widget;
|
Widget widget;
|
||||||
if (snapshot.hasData && snapshot.data != null) {
|
if (snapshot.hasData && snapshot.data != null) {
|
||||||
|
206
pigeon/api.dart
Normal file
206
pigeon/api.dart
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import 'package:pigeon/pigeon.dart';
|
||||||
|
|
||||||
|
@ConfigurePigeon(
|
||||||
|
PigeonOptions(
|
||||||
|
dartOut: 'lib/service/pigeon/api.g.dart',
|
||||||
|
kotlinOut: 'android/app/src/main/kotlin/org/moxxy/moxxyv2/Api.kt',
|
||||||
|
kotlinOptions: KotlinOptions(
|
||||||
|
package: 'org.moxxy.moxxyv2',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class NotificationMessageContent {
|
||||||
|
const NotificationMessageContent(
|
||||||
|
this.body,
|
||||||
|
this.mime,
|
||||||
|
this.path,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The textual body of the message.
|
||||||
|
final String? body;
|
||||||
|
|
||||||
|
/// The path and mime type of the media to show.
|
||||||
|
final String? mime;
|
||||||
|
final String? path;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationMessage {
|
||||||
|
const NotificationMessage(
|
||||||
|
this.sender,
|
||||||
|
this.content,
|
||||||
|
this.jid,
|
||||||
|
this.timestamp,
|
||||||
|
this.avatarPath, {
|
||||||
|
this.groupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The grouping key for the notification.
|
||||||
|
final String? groupId;
|
||||||
|
|
||||||
|
/// The sender of the message.
|
||||||
|
final String? sender;
|
||||||
|
|
||||||
|
/// The jid of the sender.
|
||||||
|
final String? jid;
|
||||||
|
|
||||||
|
/// The body of the message.
|
||||||
|
final NotificationMessageContent content;
|
||||||
|
|
||||||
|
/// Milliseconds since epoch.
|
||||||
|
final int timestamp;
|
||||||
|
|
||||||
|
/// The path to the avatar to use
|
||||||
|
final String? avatarPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessagingNotification {
|
||||||
|
const MessagingNotification(this.title, this.id, this.jid, this.messages,
|
||||||
|
this.channelId, this.isGroupchat, this.extra,
|
||||||
|
{this.groupId});
|
||||||
|
|
||||||
|
/// The title of the conversation.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The id of the notification.
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
/// The id of the notification channel the notification should appear on.
|
||||||
|
final String channelId;
|
||||||
|
|
||||||
|
/// The JID of the chat in which the notifications happen.
|
||||||
|
final String jid;
|
||||||
|
|
||||||
|
/// Messages to show.
|
||||||
|
final List<NotificationMessage?> messages;
|
||||||
|
|
||||||
|
/// Flag indicating whether this notification is from a groupchat or not.
|
||||||
|
final bool isGroupchat;
|
||||||
|
|
||||||
|
/// The id for notification grouping.
|
||||||
|
final String? groupId;
|
||||||
|
|
||||||
|
/// Additional data to include.
|
||||||
|
final Map<String?, String?>? extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationIcon {
|
||||||
|
warning,
|
||||||
|
error,
|
||||||
|
none,
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegularNotification {
|
||||||
|
const RegularNotification(
|
||||||
|
this.title, this.body, this.channelId, this.id, this.icon,
|
||||||
|
{this.groupId});
|
||||||
|
|
||||||
|
/// The title of the notification.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The body of the notification.
|
||||||
|
final String body;
|
||||||
|
|
||||||
|
/// The id of the channel to show the notification on.
|
||||||
|
final String channelId;
|
||||||
|
|
||||||
|
/// The id for notification grouping.
|
||||||
|
final String? groupId;
|
||||||
|
|
||||||
|
/// The id of the notification.
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
/// The icon to use.
|
||||||
|
final NotificationIcon icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationEventType {
|
||||||
|
markAsRead,
|
||||||
|
reply,
|
||||||
|
open,
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationEvent {
|
||||||
|
const NotificationEvent(
|
||||||
|
this.id,
|
||||||
|
this.jid,
|
||||||
|
this.type,
|
||||||
|
this.payload,
|
||||||
|
this.extra,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The notification id.
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
/// The JID the notification was for.
|
||||||
|
final String jid;
|
||||||
|
|
||||||
|
/// The type of event.
|
||||||
|
final NotificationEventType type;
|
||||||
|
|
||||||
|
/// An optional payload.
|
||||||
|
/// - type == NotificationType.reply: The reply message text.
|
||||||
|
/// Otherwise: undefined.
|
||||||
|
final String? payload;
|
||||||
|
|
||||||
|
/// Extra data. Only set when type == NotificationType.reply.
|
||||||
|
final Map<String?, String?>? extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationI18nData {
|
||||||
|
const NotificationI18nData(this.reply, this.markAsRead, this.you);
|
||||||
|
|
||||||
|
/// The content of the reply button.
|
||||||
|
final String reply;
|
||||||
|
|
||||||
|
/// The content of the "mark as read" button.
|
||||||
|
final String markAsRead;
|
||||||
|
|
||||||
|
/// The text to show when *you* reply.
|
||||||
|
final String you;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationGroup {
|
||||||
|
const NotificationGroup(this.id, this.description);
|
||||||
|
final String id;
|
||||||
|
final String description;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationChannelImportance { MIN, HIGH, DEFAULT }
|
||||||
|
|
||||||
|
class NotificationChannel {
|
||||||
|
const NotificationChannel(
|
||||||
|
this.id,
|
||||||
|
this.title,
|
||||||
|
this.description, {
|
||||||
|
this.importance = NotificationChannelImportance.DEFAULT,
|
||||||
|
this.showBadge = true,
|
||||||
|
this.groupId,
|
||||||
|
this.vibration = true,
|
||||||
|
this.enableLights = true,
|
||||||
|
});
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final String id;
|
||||||
|
final NotificationChannelImportance importance;
|
||||||
|
final bool showBadge;
|
||||||
|
final String? groupId;
|
||||||
|
final bool vibration;
|
||||||
|
final bool enableLights;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostApi()
|
||||||
|
abstract class MoxxyApi {
|
||||||
|
/// Notification APIs
|
||||||
|
void createNotificationGroups(List<NotificationGroup> groups);
|
||||||
|
void deleteNotificationGroups(List<String> ids);
|
||||||
|
void createNotificationChannels(List<NotificationChannel> channels);
|
||||||
|
void deleteNotificationChannels(List<String> ids);
|
||||||
|
void showMessagingNotification(MessagingNotification notification);
|
||||||
|
void showNotification(RegularNotification notification);
|
||||||
|
void dismissNotification(int id);
|
||||||
|
void setNotificationSelfAvatar(String path);
|
||||||
|
void setNotificationI18n(NotificationI18nData data);
|
||||||
|
|
||||||
|
// Stubs for generating event classes
|
||||||
|
void notificationStub(NotificationEvent event);
|
||||||
|
}
|
95
pubspec.lock
95
pubspec.lock
@ -5,18 +5,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "8745ddb5f27423c6ba4cc3b182688407239fe38f73ef93a0db0a3497ddf4c2e6"
|
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "46.0.0"
|
version: "61.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "2c93c461a00a27dad2849137304d32b3c6b79c75b1d10ec9547ce329de329524"
|
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.6.0"
|
version: "5.13.0"
|
||||||
archive:
|
archive:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -57,14 +57,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
awesome_notifications:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: awesome_notifications
|
|
||||||
sha256: "2b430c75cc879d6cfd52bb6eb2b5c1591ed425347816408cdcbd3f6916bba14c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.4+1"
|
|
||||||
badges:
|
badges:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -101,18 +93,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: "29a03af98de60b4eb9136acd56608a54e989f6da238a80af739415b05589d6df"
|
sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.1"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_config
|
name: build_config
|
||||||
sha256: ad77deb6e9c143a3f550fbb4c5c1e0c6aadabe24274898d06b9526c61b9cf4fb
|
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.1.1"
|
||||||
build_daemon:
|
build_daemon:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -125,18 +117,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
sha256: "9aae031a54ab0beebc30a888c93e900d15ae2fd8883d031dbfbd5ebdb57f5a4c"
|
sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.9"
|
version: "2.2.1"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "361d73f37cd48c47a81a61421eb1cc4cfd2324516fbb52f1bc4c9a01834ef2de"
|
sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.11"
|
version: "2.3.3"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -365,10 +357,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "8aff82f9b26fd868992e5430335a9d773bfef01e1d852d7ba71bf4c5d9349351"
|
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.3.2"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -652,10 +644,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: freezed
|
name: freezed
|
||||||
sha256: b8f1017d491344ef41045d3fe85950404c49e74eeaf84a84d7b8b67ac24dfb91
|
sha256: "20db669e3663fd0cbfb53729e513199fb08627ae40de0db85e0b0fe32f82bf82"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0+1"
|
version: "2.1.1"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -668,10 +660,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: frontend_server_client
|
name: frontend_server_client
|
||||||
sha256: "4f4a162323c86ffc1245765cfe138872b8f069deb42f7dbb36115fa27f31469b"
|
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "3.2.0"
|
||||||
get_it:
|
get_it:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -780,10 +772,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: json_serializable
|
name: json_serializable
|
||||||
sha256: "0cec7060459254cf1ff980c08dedca6fa50917724a3c3ec8c5026cb88dee8238"
|
sha256: fd1bcfbf6f623e1dfcc60616f189a6ca540dba7b5917447be5dab754b3116932
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.1"
|
version: "6.3.2"
|
||||||
keyboard_height_plugin:
|
keyboard_height_plugin:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -893,26 +885,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: moxplatform
|
name: moxplatform
|
||||||
sha256: "68d96d411ce0c8ddf7f60a89b338edb88f4a9a5405545d925d1fbb784402f16a"
|
sha256: "02ac0ee17f30e2bc3914a3f177d97f02d9fd062bc69ad4d5dd17404d2829f2c7"
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.17+4"
|
version: "0.1.17+6"
|
||||||
moxplatform_android:
|
moxplatform_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: moxplatform_android
|
name: moxplatform_android
|
||||||
sha256: a0683b90030c5e4cd163dccf96c61a3aa513ace664a2c9a581d7970fbd683251
|
sha256: "6b46109ec5d2c4a43cc68ac3dcf0396e5237c5800671a7e2dbcd22f468a13e28"
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.20"
|
version: "0.1.22"
|
||||||
moxplatform_platform_interface:
|
moxplatform_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: moxplatform_platform_interface
|
name: moxplatform_platform_interface
|
||||||
sha256: "14143f2850673fcb56535380718aec7d7d1f817986851a29fd83a905c1ea94fd"
|
sha256: "27a6691d6913a291f7bc93d98c87eef491a4e47a76d3d804d1e0a2de7a5875f0"
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.20"
|
version: "0.1.22"
|
||||||
moxxmpp:
|
moxxmpp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -933,10 +925,9 @@ packages:
|
|||||||
moxxyv2_builders:
|
moxxyv2_builders:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: moxxyv2_builders
|
path: "../moxxyv2_builders"
|
||||||
sha256: f936974cfc6f80308ecd348889e365feed164887cb576caf8c82acadfa7cc0c9
|
relative: true
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
source: path
|
||||||
source: hosted
|
|
||||||
version: "0.2.0"
|
version: "0.2.0"
|
||||||
native_imaging:
|
native_imaging:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
@ -1122,6 +1113,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
pigeon:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: pigeon
|
||||||
|
sha256: "5a79fd0b10423f6b5705525e32015597f861c31220b522a67d1e6b580da96719"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.1"
|
||||||
pinenacl:
|
pinenacl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1451,18 +1450,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "00f8b6b586f724a8c769c96f1d517511a41661c0aede644544d8d86a1ab11142"
|
sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.3.2"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_helper
|
name: source_helper
|
||||||
sha256: "522d9b05c40ec14f479ce4428337d106c0465fedab42f514582c198460a784fe"
|
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.4"
|
||||||
source_map_stack_trace:
|
source_map_stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1712,14 +1711,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0+1"
|
version: "4.0.0+1"
|
||||||
video_thumbnail:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: video_thumbnail
|
|
||||||
sha256: "3455c189d3f0bb4e3fc2236475aa84fe598b9b2d0e08f43b9761f5bc44210016"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.5.3"
|
|
||||||
visibility_detector:
|
visibility_detector:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1788,10 +1779,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: yaml
|
name: yaml
|
||||||
sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace"
|
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.0 <3.0.0"
|
dart: ">=2.19.0 <3.0.0"
|
||||||
flutter: ">=3.7.3"
|
flutter: ">=3.7.3"
|
||||||
|
19
pubspec.yaml
19
pubspec.yaml
@ -13,7 +13,6 @@ dependencies:
|
|||||||
archive: 3.3.2
|
archive: 3.3.2
|
||||||
audiofileplayer: 2.1.1
|
audiofileplayer: 2.1.1
|
||||||
auto_size_text: 3.0.0
|
auto_size_text: 3.0.0
|
||||||
awesome_notifications: 0.7.4+1
|
|
||||||
badges: 2.0.3
|
badges: 2.0.3
|
||||||
better_open_file: 3.6.3
|
better_open_file: 3.6.3
|
||||||
bloc: 8.1.0
|
bloc: 8.1.0
|
||||||
@ -65,7 +64,7 @@ dependencies:
|
|||||||
version: ^0.2.0
|
version: ^0.2.0
|
||||||
moxplatform:
|
moxplatform:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.1.17+4
|
version: 0.1.17+6
|
||||||
moxxmpp:
|
moxxmpp:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
@ -103,11 +102,10 @@ dependencies:
|
|||||||
url_launcher: 6.1.5
|
url_launcher: 6.1.5
|
||||||
#unifiedpush: 3.0.1
|
#unifiedpush: 3.0.1
|
||||||
uuid: 3.0.5
|
uuid: 3.0.5
|
||||||
video_thumbnail: 0.5.3
|
|
||||||
visibility_detector: 0.4.0+2
|
visibility_detector: 0.4.0+2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.1.11
|
build_runner: ^2.3.3
|
||||||
flutter_launcher_icons: ^0.9.3
|
flutter_launcher_icons: ^0.9.3
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
#flutter_test:
|
#flutter_test:
|
||||||
@ -116,6 +114,7 @@ dev_dependencies:
|
|||||||
#integration_test:
|
#integration_test:
|
||||||
# sdk: flutter
|
# sdk: flutter
|
||||||
json_serializable: ^6.3.1
|
json_serializable: ^6.3.1
|
||||||
|
pigeon: 11.0.1
|
||||||
slang_build_runner: 3.4.0
|
slang_build_runner: 3.4.0
|
||||||
test: ^1.21.1
|
test: ^1.21.1
|
||||||
very_good_analysis: ^4.0.0
|
very_good_analysis: ^4.0.0
|
||||||
@ -133,14 +132,21 @@ dependency_overrides:
|
|||||||
# path: ../moxxmpp/packages/moxxmpp_socket_tcp
|
# path: ../moxxmpp/packages/moxxmpp_socket_tcp
|
||||||
# omemo_dart:
|
# omemo_dart:
|
||||||
# path: ../../Personal/omemo_dart
|
# path: ../../Personal/omemo_dart
|
||||||
|
# moxplatform:
|
||||||
|
# path: ../moxplatform/packages/moxplatform
|
||||||
# moxplatform_android:
|
# moxplatform_android:
|
||||||
# path: ../moxplatform/packages/moxplatform_android
|
# path: ../moxplatform/packages/moxplatform_android
|
||||||
|
# moxplatform_platform_interface:
|
||||||
|
# path: ../moxplatform/packages/moxplatform_platform_interface
|
||||||
|
|
||||||
moxxmpp:
|
moxxmpp:
|
||||||
git:
|
git:
|
||||||
url: https://codeberg.org/moxxy/moxxmpp.git
|
url: https://codeberg.org/moxxy/moxxmpp.git
|
||||||
rev: 864cc0e7474d98f691d87b7a0806f428bf98b790
|
rev: 864cc0e7474d98f691d87b7a0806f428bf98b790
|
||||||
path: packages/moxxmpp
|
path: packages/moxxmpp
|
||||||
|
|
||||||
|
moxxyv2_builders:
|
||||||
|
path: ../moxxyv2_builders
|
||||||
|
|
||||||
extra_licenses:
|
extra_licenses:
|
||||||
- name: undraw.co
|
- name: undraw.co
|
||||||
@ -152,6 +158,11 @@ extra_licenses:
|
|||||||
url: "https://invent.kde.org/melvo/xmpp-providers"
|
url: "https://invent.kde.org/melvo/xmpp-providers"
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
android:
|
||||||
|
package: org.moxxy.moxxyv2
|
||||||
|
pluginClass: MainActivity
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
fonts:
|
fonts:
|
||||||
- family: RobotoMono
|
- family: RobotoMono
|
||||||
|
@ -14,20 +14,21 @@ void main() {
|
|||||||
Logger('TestLogger'),
|
Logger('TestLogger'),
|
||||||
1,
|
1,
|
||||||
[
|
[
|
||||||
DatabaseMigration(4, (_) async {
|
Migration(4, (_) async {
|
||||||
expect(counter, 3);
|
expect(counter, 3);
|
||||||
counter++;
|
counter++;
|
||||||
}),
|
}),
|
||||||
DatabaseMigration(2, (_) async {
|
Migration(2, (_) async {
|
||||||
expect(counter, 1);
|
expect(counter, 1);
|
||||||
counter++;
|
counter++;
|
||||||
}),
|
}),
|
||||||
DatabaseMigration(3, (_) async {
|
Migration(3, (_) async {
|
||||||
expect(counter, 2);
|
expect(counter, 2);
|
||||||
counter++;
|
counter++;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
1,
|
1,
|
||||||
|
'test',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,21 +38,63 @@ void main() {
|
|||||||
Logger('TestLogger'),
|
Logger('TestLogger'),
|
||||||
1,
|
1,
|
||||||
[
|
[
|
||||||
DatabaseMigration(4, (_) async {
|
Migration(4, (_) async {
|
||||||
expect(counter, 3);
|
expect(counter, 3);
|
||||||
counter++;
|
counter++;
|
||||||
}),
|
}),
|
||||||
DatabaseMigration(2, (_) async {
|
Migration(2, (_) async {
|
||||||
// This must never be called
|
// This must never be called
|
||||||
expect(true, false);
|
expect(true, false);
|
||||||
}),
|
}),
|
||||||
DatabaseMigration(3, (_) async {
|
Migration(3, (_) async {
|
||||||
expect(counter, 2);
|
expect(counter, 2);
|
||||||
counter++;
|
counter++;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
2,
|
2,
|
||||||
|
'test',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Commit when a migration has run', () async {
|
||||||
|
var hasRun = false;
|
||||||
|
await runMigrations<int>(
|
||||||
|
Logger('TestLogger'),
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
Migration(4, (_) async {}),
|
||||||
|
Migration(2, (_) async {}),
|
||||||
|
Migration(3, (_) async {}),
|
||||||
|
],
|
||||||
|
2,
|
||||||
|
'test',
|
||||||
|
commitVersion: (version) async {
|
||||||
|
expect(version, 4);
|
||||||
|
hasRun = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(hasRun, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Do not commit when no migration has run', () async {
|
||||||
|
var hasRun = false;
|
||||||
|
await runMigrations<int>(
|
||||||
|
Logger('TestLogger'),
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
Migration(4, (_) async {}),
|
||||||
|
Migration(2, (_) async {}),
|
||||||
|
Migration(3, (_) async {}),
|
||||||
|
],
|
||||||
|
4,
|
||||||
|
'test',
|
||||||
|
commitVersion: (version) async {
|
||||||
|
hasRun = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(hasRun, false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user