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-hard-tab]
|
||||
[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]
|
||||
|
@ -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
|
||||
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
|
||||
#### `data_classes.yaml`
|
||||
|
||||
|
@ -14,3 +14,4 @@ analyzer:
|
||||
- "**/*.freezed.dart"
|
||||
- "**/*.moxxy.dart"
|
||||
- "lib/i18n/*.dart"
|
||||
- "pigeon/api.dart"
|
||||
|
@ -1,33 +1,35 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.moxxy.moxxyv2">
|
||||
<application
|
||||
android:label="Moxxy"
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Moxxy">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Allow receiving share intents for all kinds of things -->
|
||||
<!-- Allow receiving share intents for all kinds of things -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@ -47,20 +49,37 @@
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/share_targets" />
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
</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
|
||||
|
||||
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.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",
|
||||
"messagesChannelDescription": "The notification channel for received messages",
|
||||
"warningChannelName": "Warnings",
|
||||
"warningChannelDescription": "Warnings related to Moxxy"
|
||||
"warningChannelDescription": "Warnings related to Moxxy",
|
||||
"serviceChannelName": "Foreground Service",
|
||||
"serviceChannelDescription": "Holds the persistent foreground service notification"
|
||||
},
|
||||
"titles": {
|
||||
"error": "Error"
|
||||
|
@ -1,6 +1,12 @@
|
||||
- (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.
|
||||
- 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.
|
||||
- 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 {
|
||||
buildInputs = with pkgs; [
|
||||
# Android
|
||||
pinnedJDK sdk
|
||||
pinnedJDK sdk ktlint
|
||||
scrcpy
|
||||
|
||||
# Flutter
|
||||
|
@ -481,6 +481,12 @@ files:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
jid: String
|
||||
- name: ExitConversationCommand
|
||||
extends: BackgroundCommand
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
conversationType: String
|
||||
- name: SendChatStateCommand
|
||||
extends: BackgroundCommand
|
||||
implements:
|
||||
|
@ -89,7 +89,7 @@ class AvatarService {
|
||||
final rs = GetIt.I.get<RosterService>();
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
final originalConversation =
|
||||
await cs.getConversationByJid(jid.toString(), accountJid);
|
||||
await cs.getConversationByJid(jid.toString(), accountJid!);
|
||||
final originalRoster = await rs.getRosterItemByJid(
|
||||
jid.toString(),
|
||||
accountJid,
|
||||
@ -197,8 +197,9 @@ class AvatarService {
|
||||
/// Like [requestAvatar], but fetches and processes the avatar for our own account.
|
||||
Future<void> requestOwnAvatar() async {
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final state = await xss.getXmppState();
|
||||
final jid = JID.fromString(state.jid!);
|
||||
final accountJid = await xss.getAccountJid();
|
||||
final state = await xss.state;
|
||||
final jid = JID.fromString(accountJid!);
|
||||
|
||||
if (_requestedInStream.contains(jid)) {
|
||||
return;
|
||||
|
@ -72,7 +72,7 @@ class BlocklistService {
|
||||
final removedItems = List<String>.empty(growable: true);
|
||||
for (final item in blocklist) {
|
||||
if (!_blocklist!.contains(item)) {
|
||||
await _addBlocklistEntry(item, accountJid);
|
||||
await _addBlocklistEntry(item, accountJid!);
|
||||
_blocklist!.add(item);
|
||||
newItems.add(item);
|
||||
}
|
||||
@ -81,7 +81,7 @@ class BlocklistService {
|
||||
// Diff the cache with the received blocklist
|
||||
for (final item in _blocklist!) {
|
||||
if (!blocklist.contains(item)) {
|
||||
await _removeBlocklistEntry(item, accountJid);
|
||||
await _removeBlocklistEntry(item, accountJid!);
|
||||
_blocklist!.remove(item);
|
||||
removedItems.add(item);
|
||||
}
|
||||
@ -146,7 +146,7 @@ class BlocklistService {
|
||||
_blocklist!.add(item);
|
||||
newBlocks.add(item);
|
||||
|
||||
await _addBlocklistEntry(item, accountJid);
|
||||
await _addBlocklistEntry(item, accountJid!);
|
||||
}
|
||||
break;
|
||||
case BlockPushType.unblock:
|
||||
@ -154,7 +154,7 @@ class BlocklistService {
|
||||
_blocklist!.removeWhere((i) => i == item);
|
||||
removedBlocks.add(item);
|
||||
|
||||
await _removeBlocklistEntry(item, accountJid);
|
||||
await _removeBlocklistEntry(item, accountJid!);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -178,7 +178,7 @@ class BlocklistService {
|
||||
_blocklist!.add(jid);
|
||||
await _addBlocklistEntry(
|
||||
jid,
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
);
|
||||
return GetIt.I
|
||||
.get<XmppConnection>()
|
||||
@ -196,7 +196,7 @@ class BlocklistService {
|
||||
_blocklist!.remove(jid);
|
||||
await _removeBlocklistEntry(
|
||||
jid,
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
);
|
||||
return GetIt.I
|
||||
.get<XmppConnection>()
|
||||
|
@ -61,7 +61,7 @@ class ContactsService {
|
||||
FlutterContacts.removeListener(_onContactsDatabaseUpdate);
|
||||
|
||||
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
|
||||
final conversation = await cs.createOrUpdateConversation(
|
||||
jid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
update: (c) async {
|
||||
return cs.updateConversation(
|
||||
jid,
|
||||
@ -284,7 +284,7 @@ class ContactsService {
|
||||
// Update a possibly existing conversation
|
||||
final conversation = await cs.createOrUpdateConversation(
|
||||
contact.jid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
update: (c) async {
|
||||
return cs.updateConversation(
|
||||
contact.jid,
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxyv2/service/connectivity.dart';
|
||||
import 'package:moxxyv2/service/database/constants.dart';
|
||||
import 'package:moxxyv2/service/database/database.dart';
|
||||
import 'package:moxxyv2/service/database/helpers.dart';
|
||||
@ -28,6 +30,17 @@ class ConversationService {
|
||||
/// The lock for accessing _conversationCache
|
||||
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
|
||||
/// executed.
|
||||
/// 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);
|
||||
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';
|
||||
|
||||
@internal
|
||||
const List<DatabaseMigration<Database>> migrations = [
|
||||
DatabaseMigration(2, upgradeFromV1ToV2),
|
||||
DatabaseMigration(3, upgradeFromV2ToV3),
|
||||
DatabaseMigration(4, upgradeFromV3ToV4),
|
||||
DatabaseMigration(5, upgradeFromV4ToV5),
|
||||
DatabaseMigration(6, upgradeFromV5ToV6),
|
||||
DatabaseMigration(7, upgradeFromV6ToV7),
|
||||
DatabaseMigration(8, upgradeFromV7ToV8),
|
||||
DatabaseMigration(9, upgradeFromV8ToV9),
|
||||
DatabaseMigration(10, upgradeFromV9ToV10),
|
||||
DatabaseMigration(11, upgradeFromV10ToV11),
|
||||
DatabaseMigration(12, upgradeFromV11ToV12),
|
||||
DatabaseMigration(13, upgradeFromV12ToV13),
|
||||
DatabaseMigration(14, upgradeFromV13ToV14),
|
||||
DatabaseMigration(15, upgradeFromV14ToV15),
|
||||
DatabaseMigration(16, upgradeFromV15ToV16),
|
||||
DatabaseMigration(17, upgradeFromV16ToV17),
|
||||
DatabaseMigration(18, upgradeFromV17ToV18),
|
||||
DatabaseMigration(19, upgradeFromV18ToV19),
|
||||
DatabaseMigration(20, upgradeFromV19ToV20),
|
||||
DatabaseMigration(21, upgradeFromV20ToV21),
|
||||
DatabaseMigration(22, upgradeFromV21ToV22),
|
||||
DatabaseMigration(23, upgradeFromV22ToV23),
|
||||
DatabaseMigration(24, upgradeFromV23ToV24),
|
||||
DatabaseMigration(25, upgradeFromV24ToV25),
|
||||
DatabaseMigration(26, upgradeFromV25ToV26),
|
||||
DatabaseMigration(27, upgradeFromV26ToV27),
|
||||
DatabaseMigration(28, upgradeFromV27ToV28),
|
||||
DatabaseMigration(29, upgradeFromV28ToV29),
|
||||
DatabaseMigration(30, upgradeFromV29ToV30),
|
||||
DatabaseMigration(31, upgradeFromV30ToV31),
|
||||
DatabaseMigration(32, upgradeFromV31ToV32),
|
||||
DatabaseMigration(33, upgradeFromV32ToV33),
|
||||
DatabaseMigration(34, upgradeFromV33ToV34),
|
||||
DatabaseMigration(35, upgradeFromV34ToV35),
|
||||
DatabaseMigration(36, upgradeFromV35ToV36),
|
||||
DatabaseMigration(37, upgradeFromV36ToV37),
|
||||
DatabaseMigration(38, upgradeFromV37ToV38),
|
||||
DatabaseMigration(39, upgradeFromV38ToV39),
|
||||
DatabaseMigration(40, upgradeFromV39ToV40),
|
||||
DatabaseMigration(41, upgradeFromV40ToV41),
|
||||
DatabaseMigration(42, upgradeFromV41ToV42),
|
||||
DatabaseMigration(43, upgradeFromV42ToV43),
|
||||
DatabaseMigration(44, upgradeFromV43ToV44),
|
||||
DatabaseMigration(45, upgradeFromV44ToV45),
|
||||
DatabaseMigration(46, upgradeFromV45ToV46),
|
||||
DatabaseMigration(47, upgradeFromV46ToV47),
|
||||
const List<Migration<Database>> migrations = [
|
||||
Migration(2, upgradeFromV1ToV2),
|
||||
Migration(3, upgradeFromV2ToV3),
|
||||
Migration(4, upgradeFromV3ToV4),
|
||||
Migration(5, upgradeFromV4ToV5),
|
||||
Migration(6, upgradeFromV5ToV6),
|
||||
Migration(7, upgradeFromV6ToV7),
|
||||
Migration(8, upgradeFromV7ToV8),
|
||||
Migration(9, upgradeFromV8ToV9),
|
||||
Migration(10, upgradeFromV9ToV10),
|
||||
Migration(11, upgradeFromV10ToV11),
|
||||
Migration(12, upgradeFromV11ToV12),
|
||||
Migration(13, upgradeFromV12ToV13),
|
||||
Migration(14, upgradeFromV13ToV14),
|
||||
Migration(15, upgradeFromV14ToV15),
|
||||
Migration(16, upgradeFromV15ToV16),
|
||||
Migration(17, upgradeFromV16ToV17),
|
||||
Migration(18, upgradeFromV17ToV18),
|
||||
Migration(19, upgradeFromV18ToV19),
|
||||
Migration(20, upgradeFromV19ToV20),
|
||||
Migration(21, upgradeFromV20ToV21),
|
||||
Migration(22, upgradeFromV21ToV22),
|
||||
Migration(23, upgradeFromV22ToV23),
|
||||
Migration(24, upgradeFromV23ToV24),
|
||||
Migration(25, upgradeFromV24ToV25),
|
||||
Migration(26, upgradeFromV25ToV26),
|
||||
Migration(27, upgradeFromV26ToV27),
|
||||
Migration(28, upgradeFromV27ToV28),
|
||||
Migration(29, upgradeFromV28ToV29),
|
||||
Migration(30, upgradeFromV29ToV30),
|
||||
Migration(31, upgradeFromV30ToV31),
|
||||
Migration(32, upgradeFromV31ToV32),
|
||||
Migration(33, upgradeFromV32ToV33),
|
||||
Migration(34, upgradeFromV33ToV34),
|
||||
Migration(35, upgradeFromV34ToV35),
|
||||
Migration(36, upgradeFromV35ToV36),
|
||||
Migration(37, upgradeFromV36ToV37),
|
||||
Migration(38, upgradeFromV37ToV38),
|
||||
Migration(39, upgradeFromV38ToV39),
|
||||
Migration(40, upgradeFromV39ToV40),
|
||||
Migration(41, upgradeFromV40ToV41),
|
||||
Migration(42, upgradeFromV41ToV42),
|
||||
Migration(43, upgradeFromV42ToV43),
|
||||
Migration(44, upgradeFromV43ToV44),
|
||||
Migration(45, upgradeFromV44ToV45),
|
||||
Migration(46, upgradeFromV45ToV46),
|
||||
Migration(47, upgradeFromV46ToV47),
|
||||
];
|
||||
|
||||
class DatabaseService {
|
||||
@ -150,7 +150,7 @@ class DatabaseService {
|
||||
await db.execute('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
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';
|
||||
|
||||
/// 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.
|
||||
class DatabaseMigration<T> {
|
||||
const DatabaseMigration(this.version, this.migration);
|
||||
class Migration<T> {
|
||||
const Migration(this.version, this.migration);
|
||||
|
||||
/// The version this migration upgrades the database to.
|
||||
final int 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
|
||||
/// [version]. [migrations] is sorted before usage.
|
||||
///
|
||||
@ -23,22 +24,32 @@ class DatabaseMigration<T> {
|
||||
/// database argument, just pass in whatever (the tests use an integer).
|
||||
Future<void> runMigrations<T>(
|
||||
Logger log,
|
||||
T db,
|
||||
List<DatabaseMigration<T>> migrations,
|
||||
T param,
|
||||
List<Migration<T>> migrations,
|
||||
int version,
|
||||
) async {
|
||||
final sortedMigrations = List<DatabaseMigration<T>>.from(migrations)
|
||||
String typeName, {
|
||||
Future<void> Function(int)? commitVersion,
|
||||
}) async {
|
||||
final sortedMigrations = List<Migration<T>>.from(migrations)
|
||||
..sort(
|
||||
(a, b) => a.version.compareTo(b.version),
|
||||
);
|
||||
var currentVersion = version;
|
||||
var hasRunMigration = false;
|
||||
for (final migration in sortedMigrations) {
|
||||
if (version < migration.version) {
|
||||
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;
|
||||
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',
|
||||
{
|
||||
...trustItem,
|
||||
'accoutJid': accountJid,
|
||||
'accountJid': accountJid,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/service/avatars.dart';
|
||||
import 'package:moxxyv2/service/blocking.dart';
|
||||
import 'package:moxxyv2/service/connectivity.dart';
|
||||
import 'package:moxxyv2/service/contacts.dart';
|
||||
import 'package:moxxyv2/service/conversation.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/location.dart';
|
||||
import 'package:moxxyv2/service/language.dart';
|
||||
import 'package:moxxyv2/service/lifecycle.dart';
|
||||
import 'package:moxxyv2/service/message.dart';
|
||||
import 'package:moxxyv2/service/notifications.dart';
|
||||
import 'package:moxxyv2/service/omemo/omemo.dart';
|
||||
@ -117,6 +117,7 @@ void setupBackgroundEventHandler() {
|
||||
EventTypeMatcher<FetchRecipientInformationCommand>(
|
||||
performFetchRecipientInformation,
|
||||
),
|
||||
EventTypeMatcher<ExitConversationCommand>(performConversationExited),
|
||||
]);
|
||||
|
||||
GetIt.I.registerSingleton<EventHandler>(handler);
|
||||
@ -128,6 +129,17 @@ void setupBackgroundEventHandler() {
|
||||
Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
|
||||
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...');
|
||||
final result = await GetIt.I.get<XmppService>().connectAwaitable(
|
||||
ConnectionSettings(
|
||||
@ -141,14 +153,25 @@ Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
|
||||
// ignore: avoid_dynamic_calls
|
||||
final xc = GetIt.I.get<XmppConnection>();
|
||||
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 =
|
||||
await GetIt.I.get<PreferencesService>().getPreferences();
|
||||
final settings = xc.connectionSettings;
|
||||
final state = await xss.state;
|
||||
|
||||
sendEvent(
|
||||
LoginSuccessfulEvent(
|
||||
jid: settings.jid.toString(),
|
||||
preStart: await _buildPreStartDoneEvent(preferences),
|
||||
preStart: await _buildPreStartDoneEvent(
|
||||
state,
|
||||
command.jid,
|
||||
preferences,
|
||||
),
|
||||
),
|
||||
id: id,
|
||||
);
|
||||
@ -165,12 +188,10 @@ Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
|
||||
}
|
||||
|
||||
Future<PreStartDoneEvent> _buildPreStartDoneEvent(
|
||||
XmppState state,
|
||||
String accountJid,
|
||||
PreferencesState preferences,
|
||||
) async {
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final accountJid = await xss.getAccountJid();
|
||||
final state = await xss.getXmppState();
|
||||
|
||||
await GetIt.I.get<RosterService>().loadRosterFromDatabase(accountJid);
|
||||
|
||||
return PreStartDoneEvent(
|
||||
@ -203,11 +224,16 @@ Future<void> performPreStart(
|
||||
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||
|
||||
// Set the locale very early
|
||||
final logger = GetIt.I.get<Logger>();
|
||||
GetIt.I.get<LanguageService>().defaultLocale = command.systemLocaleCode;
|
||||
if (preferences.languageLocaleCode == 'default') {
|
||||
LocaleSettings.setLocaleRaw(command.systemLocaleCode);
|
||||
logger.finest('Setting locale to default (${command.systemLocaleCode})');
|
||||
} else {
|
||||
LocaleSettings.setLocaleRaw(preferences.languageLocaleCode);
|
||||
logger.finest(
|
||||
'Setting locale to configured language (${preferences.languageLocaleCode})',
|
||||
);
|
||||
}
|
||||
await GetIt.I.get<NotificationsService>().configureNotificationI18n();
|
||||
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.
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getRawAccountJid();
|
||||
final isLoggedIn = accountJid != null
|
||||
? await GetIt.I.get<XmppService>().getConnectionSettings() != null
|
||||
: false;
|
||||
if (isLoggedIn) {
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final accountJid = await xss.getAccountJid();
|
||||
if (await xss.isLoggedIn(accountJid)) {
|
||||
sendEvent(
|
||||
await _buildPreStartDoneEvent(preferences),
|
||||
await _buildPreStartDoneEvent(
|
||||
await xss.state,
|
||||
accountJid!,
|
||||
preferences,
|
||||
),
|
||||
id: id,
|
||||
);
|
||||
} else {
|
||||
sendEvent(
|
||||
PreStartDoneEvent(
|
||||
state: 'not_logged_in',
|
||||
requestNotificationPermission: await GetIt.I
|
||||
.get<PermissionsService>()
|
||||
.shouldRequestNotificationPermission(),
|
||||
excludeFromBatteryOptimisation: await GetIt.I
|
||||
.get<PermissionsService>()
|
||||
.shouldRequestBatteryOptimisationExcemption(),
|
||||
requestNotificationPermission: false,
|
||||
excludeFromBatteryOptimisation: false,
|
||||
preferences: preferences,
|
||||
),
|
||||
id: id,
|
||||
@ -253,7 +277,7 @@ Future<void> performAddConversation(
|
||||
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||
await cs.createOrUpdateConversation(
|
||||
command.jid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
create: () async {
|
||||
// Create
|
||||
final contactId = await css.getContactIdForJid(command.jid);
|
||||
@ -323,7 +347,7 @@ Future<void> performSetOpenConversation(
|
||||
if (command.jid != null && command.jid != '') {
|
||||
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
|
||||
command.jid!,
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -343,7 +367,7 @@ Future<void> performSendMessage(
|
||||
await xs.sendMessageCorrection(
|
||||
command.editSid!,
|
||||
command.recipients.first,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
command.body,
|
||||
command.editSid!,
|
||||
command.recipients.first,
|
||||
@ -355,7 +379,7 @@ Future<void> performSendMessage(
|
||||
}
|
||||
|
||||
await xs.sendMessage(
|
||||
accountJid: accountJid,
|
||||
accountJid: accountJid!,
|
||||
body: command.body,
|
||||
recipients: command.recipients,
|
||||
chatState: command.chatState.isNotEmpty
|
||||
@ -390,11 +414,10 @@ Future<void> performSetCSIState(
|
||||
dynamic extra,
|
||||
}) async {
|
||||
// Tell the [XmppService] about the app state
|
||||
GetIt.I.get<XmppService>().setAppState(command.active);
|
||||
|
||||
final conn = GetIt.I.get<XmppConnection>();
|
||||
GetIt.I.get<LifecycleService>().isActive = command.active;
|
||||
|
||||
// Only send the CSI nonza when we're connected
|
||||
final conn = GetIt.I.get<XmppConnection>();
|
||||
if (await conn.getConnectionState() != XmppConnectionState.connected) return;
|
||||
final csi = conn.getManagerById<CSIManager>(csiManager)!;
|
||||
if (command.active) {
|
||||
@ -435,11 +458,12 @@ Future<void> performSetPreferences(
|
||||
|
||||
// TODO(Unknown): Maybe handle this in StickersService
|
||||
// If sticker visibility was changed, apply the settings to the PubSub node
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final pm = GetIt.I
|
||||
.get<XmppConnection>()
|
||||
.getManagerById<PubSubManager>(pubsubManager)!;
|
||||
final ownJid = JID.fromString(
|
||||
(await GetIt.I.get<XmppStateService>().getXmppState()).jid!,
|
||||
(await xss.state).jid!,
|
||||
);
|
||||
if (command.preferences.isStickersNodePublic &&
|
||||
!oldPrefs.isStickersNodePublic) {
|
||||
@ -528,7 +552,7 @@ Future<void> performAddContact(
|
||||
final jid = command.jid;
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
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 conversation = await cs.getConversationByJid(jid, accountJid);
|
||||
@ -649,7 +673,7 @@ Future<void> performRemoveContact(
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
|
||||
// Remove from roster
|
||||
await rs.removeFromRosterWrapper(command.jid, accountJid);
|
||||
await rs.removeFromRosterWrapper(command.jid, accountJid!);
|
||||
|
||||
// Update the conversation
|
||||
final conversation = await cs.getConversationByJid(command.jid, accountJid);
|
||||
@ -674,7 +698,7 @@ Future<void> performRequestDownload(
|
||||
|
||||
final message = await ms.updateMessage(
|
||||
command.message.id,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
isDownloading: true,
|
||||
);
|
||||
sendEvent(MessageUpdatedEvent(message: message));
|
||||
@ -735,7 +759,7 @@ Future<void> performSetShareOnlineStatus(
|
||||
}) async {
|
||||
final rs = GetIt.I.get<RosterService>();
|
||||
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
|
||||
if (item == null) return;
|
||||
@ -774,7 +798,7 @@ Future<void> performCloseConversation(
|
||||
|
||||
await cs.createOrUpdateConversation(
|
||||
command.jid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
update: (c) async {
|
||||
return cs.updateConversation(
|
||||
command.jid,
|
||||
@ -794,25 +818,11 @@ Future<void> performSendChatState(
|
||||
SendChatStateCommand command, {
|
||||
dynamic extra,
|
||||
}) 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
|
||||
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,
|
||||
);
|
||||
}
|
||||
await GetIt.I.get<ConversationService>().sendChatState(
|
||||
ConversationType.fromString(command.conversationType),
|
||||
command.jid,
|
||||
ChatState.fromName(command.state),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> performGetFeatures(
|
||||
@ -854,7 +864,9 @@ Future<void> performSignOut(SignOutCommand command, {dynamic extra}) async {
|
||||
|
||||
// Clear notifications
|
||||
final accountJid = await xss.getAccountJid();
|
||||
await GetIt.I.get<NotificationsService>().dismissAllNotifications(accountJid);
|
||||
await GetIt.I
|
||||
.get<NotificationsService>()
|
||||
.dismissAllNotifications(accountJid!);
|
||||
|
||||
// Reset the current account JID.
|
||||
await xss.resetAccountJid();
|
||||
@ -867,7 +879,7 @@ Future<void> performSignOut(SignOutCommand command, {dynamic extra}) async {
|
||||
|
||||
Future<void> performSendFiles(SendFilesCommand command, {dynamic extra}) async {
|
||||
await GetIt.I.get<XmppService>().sendFiles(
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
command.paths,
|
||||
command.recipients,
|
||||
);
|
||||
@ -882,7 +894,7 @@ Future<void> performSetMuteState(
|
||||
|
||||
final conversation = await cs.createOrUpdateConversation(
|
||||
command.jid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
update: (c) async {
|
||||
return cs.updateConversation(
|
||||
command.jid,
|
||||
@ -958,7 +970,7 @@ Future<void> performSetOmemoEnabled(
|
||||
|
||||
await cs.createOrUpdateConversation(
|
||||
command.jid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
update: (c) async {
|
||||
return cs.updateConversation(
|
||||
command.jid,
|
||||
@ -1018,7 +1030,7 @@ Future<void> performMessageRetraction(
|
||||
}) async {
|
||||
await GetIt.I.get<MessageService>().retractMessage(
|
||||
command.conversationJid,
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
command.originId,
|
||||
'',
|
||||
true,
|
||||
@ -1046,7 +1058,7 @@ Future<void> performMarkConversationAsRead(
|
||||
// Update the database
|
||||
final conversation = await cs.createOrUpdateConversation(
|
||||
command.conversationJid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
update: (c) async {
|
||||
return cs.updateConversation(
|
||||
command.conversationJid,
|
||||
@ -1070,7 +1082,7 @@ Future<void> performMarkConversationAsRead(
|
||||
// Dismiss notifications for that chat
|
||||
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
|
||||
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();
|
||||
await GetIt.I.get<MessageService>().markMessageAsRead(
|
||||
command.id,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
command.sendMarker,
|
||||
);
|
||||
}
|
||||
@ -1094,7 +1106,7 @@ Future<void> performAddMessageReaction(
|
||||
final rs = GetIt.I.get<ReactionsService>();
|
||||
final msg = await rs.addNewReaction(
|
||||
command.id,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
accountJid,
|
||||
command.emoji,
|
||||
);
|
||||
@ -1141,7 +1153,7 @@ Future<void> performRemoveMessageReaction(
|
||||
final rs = GetIt.I.get<ReactionsService>();
|
||||
final msg = await rs.removeReaction(
|
||||
command.id,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
accountJid,
|
||||
command.emoji,
|
||||
);
|
||||
@ -1217,7 +1229,7 @@ Future<void> performSendSticker(
|
||||
dynamic extra,
|
||||
}) async {
|
||||
await GetIt.I.get<XmppService>().sendMessage(
|
||||
accountJid: await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
accountJid: (await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
body: command.sticker.desc,
|
||||
recipients: [command.recipient],
|
||||
sticker: command.sticker,
|
||||
@ -1332,7 +1344,8 @@ Future<void> performGetBlocklist(
|
||||
}) async {
|
||||
final id = extra as String;
|
||||
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(
|
||||
GetBlocklistResultEvent(
|
||||
entries: result,
|
||||
@ -1349,7 +1362,7 @@ Future<void> performGetPagedMessages(
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
final result = await GetIt.I.get<MessageService>().getPaginatedMessagesForJid(
|
||||
command.conversationJid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
command.olderThan,
|
||||
command.timestamp,
|
||||
);
|
||||
@ -1370,7 +1383,7 @@ Future<void> performGetPagedSharedMedia(
|
||||
final result =
|
||||
await GetIt.I.get<MessageService>().getPaginatedSharedMediaMessagesForJid(
|
||||
command.conversationJid,
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
command.olderThan,
|
||||
command.timestamp,
|
||||
);
|
||||
@ -1392,7 +1405,7 @@ Future<void> performGetReactions(
|
||||
final reactionsRaw =
|
||||
await GetIt.I.get<ReactionsService>().getReactionsForMessage(
|
||||
command.id,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
);
|
||||
final reactionsMap = <String, List<String>>{};
|
||||
for (final reaction in reactionsRaw) {
|
||||
@ -1462,7 +1475,7 @@ Future<void> performOldMediaFileDeletion(
|
||||
newUsage: await GetIt.I.get<StorageService>().computeUsedMediaStorage(),
|
||||
conversations: (await GetIt.I
|
||||
.get<ConversationService>()
|
||||
.loadConversations(accountJid))
|
||||
.loadConversations(accountJid!))
|
||||
.where((c) => c.open)
|
||||
.toList(),
|
||||
),
|
||||
@ -1551,7 +1564,7 @@ Future<void> performJoinGroupchat(
|
||||
final nick = command.nick;
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
final conversation = await cs.getConversationByJid(jid, accountJid);
|
||||
final conversation = await cs.getConversationByJid(jid, accountJid!);
|
||||
if (conversation != null) {
|
||||
await cs.createOrUpdateConversation(
|
||||
jid,
|
||||
@ -1638,7 +1651,7 @@ Future<void> performFetchRecipientInformation(
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
for (final jid in command.jids) {
|
||||
// First try to find a roster item
|
||||
final rosterItem = await rs.getRosterItemByJid(jid, accountJid);
|
||||
final rosterItem = await rs.getRosterItemByJid(jid, accountJid!);
|
||||
if (rosterItem != null) {
|
||||
items.add(
|
||||
SendFilesRecipient(
|
||||
@ -1673,3 +1686,19 @@ Future<void> performFetchRecipientInformation(
|
||||
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/not_specified.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:sqflite_common/sql.dart';
|
||||
|
||||
@ -323,6 +324,20 @@ class FilesService {
|
||||
} catch (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 {
|
||||
_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/events.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/thumbnails/helpers.dart';
|
||||
import 'package:moxxyv2/shared/warning_types.dart';
|
||||
import 'package:path/path.dart' as pathlib;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@ -545,21 +546,33 @@ class HttpFileTransferService {
|
||||
mediaWidth = imageSize?.width.toInt();
|
||||
mediaHeight = imageSize?.height.toInt();
|
||||
} else if (mime.startsWith('video/')) {
|
||||
/*
|
||||
// Generate thumbnail
|
||||
final thumbnailPath = await getVideoThumbnailPath(
|
||||
downloadedPath,
|
||||
job.conversationJid,
|
||||
);
|
||||
if (canGenerateVideoThumbnail(mime)) {
|
||||
try {
|
||||
// Generate thumbnail
|
||||
final thumbnailPath = await maybeGenerateVideoThumbnail(
|
||||
downloadedPath,
|
||||
);
|
||||
|
||||
// Find out the dimensions
|
||||
final imageSize = await getImageSizeFromPath(thumbnailPath);
|
||||
if (imageSize == null) {
|
||||
_log.warning('Failed to get image size for $downloadedPath ($thumbnailPath)');
|
||||
if (thumbnailPath != null) {
|
||||
// Find out the dimensions
|
||||
final imageSize = await getImageSizeFromPath(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);
|
||||
|
||||
// Show a notification
|
||||
if (notification.shouldShowNotification(msg.conversationJid) &&
|
||||
job.shouldShowNotification) {
|
||||
final shouldShowNotification =
|
||||
notification.shouldShowNotification(msg.conversationJid);
|
||||
if (shouldShowNotification && job.shouldShowNotification) {
|
||||
_log.finest('Creating notification with bigPicture $downloadedPath');
|
||||
await notification.updateNotification(
|
||||
await notification.updateOrShowNotification(
|
||||
updatedConversation,
|
||||
msg,
|
||||
job.accountJid,
|
||||
);
|
||||
} else {
|
||||
_log.finest(
|
||||
'Not creating or updating notification for $downloadedPath: notification.shouldShowNotification=$shouldShowNotification, job.shouldShowNotification=${job.shouldShowNotification}',
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
final rs = GetIt.I.get<RosterService>();
|
||||
final state = await GetIt.I.get<XmppStateService>().state;
|
||||
return RosterCacheLoadResult(
|
||||
(await GetIt.I.get<XmppStateService>().getXmppState()).lastRosterVersion,
|
||||
(await rs.getRoster(accountJid))
|
||||
state.lastRosterVersion,
|
||||
(await rs.getRoster(accountJid!))
|
||||
.map(
|
||||
(item) => XmppRosterItem(
|
||||
jid: item.jid,
|
||||
@ -71,14 +72,14 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
||||
|
||||
// Remove stale items
|
||||
for (final jid in removed) {
|
||||
await rs.removeRosterItem(jid, accountJid);
|
||||
await rs.removeRosterItem(jid, accountJid!);
|
||||
await updateConversation(jid, accountJid, true);
|
||||
}
|
||||
|
||||
// Create new roster items
|
||||
final rosterAdded = List<RosterItem>.empty(growable: true);
|
||||
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
|
||||
if (exists) continue;
|
||||
|
||||
@ -109,7 +110,7 @@ class MoxxyRosterStateManager extends BaseRosterStateManager {
|
||||
// Update modified items
|
||||
final rosterModified = List<RosterItem>.empty(growable: true);
|
||||
for (final item in modified) {
|
||||
final ritem = await rs.getRosterItemByJid(item.jid, accountJid);
|
||||
final ritem = await rs.getRosterItemByJid(item.jid, accountJid!);
|
||||
if (ritem == null) {
|
||||
//_log.warning('Could not find roster item with JID $jid during update');
|
||||
continue;
|
||||
|
@ -30,7 +30,8 @@ class MoxxyStreamManagementManager extends StreamManagementManager {
|
||||
|
||||
@override
|
||||
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) {
|
||||
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 'package:flutter/services.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/service/conversation.dart';
|
||||
import 'package:moxxyv2/service/database/constants.dart';
|
||||
import 'package:moxxyv2/service/database/database.dart';
|
||||
import 'package:moxxyv2/service/lifecycle.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/xmpp.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/events.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/conversation.dart' as modelc;
|
||||
import 'package:moxxyv2/shared/models/message.dart' as modelm;
|
||||
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:sqflite_sqlcipher/sqflite.dart';
|
||||
|
||||
const _maxNotificationId = 2147483647;
|
||||
const _messageChannelKey = 'message_channel';
|
||||
const _warningChannelKey = 'warning_channel';
|
||||
|
||||
/// Message payload keys.
|
||||
const _conversationJidKey = 'conversationJid';
|
||||
@ -31,14 +34,27 @@ const _conversationTitleKey = 'title';
|
||||
const _conversationAvatarKey = 'avatarPath';
|
||||
|
||||
class NotificationsService {
|
||||
NotificationsService() {
|
||||
_eventStream = _channel
|
||||
.receiveBroadcastStream()
|
||||
.cast<Object>()
|
||||
.map(api.NotificationEvent.decode);
|
||||
}
|
||||
|
||||
/// Logging.
|
||||
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
|
||||
/// the notification has been tapped.
|
||||
Future<void> onNotificationEvent(NotificationEvent event) async {
|
||||
Future<void> onNotificationEvent(api.NotificationEvent event) async {
|
||||
final conversationJid = event.extra![_conversationJidKey]!;
|
||||
if (event.type == NotificationEventType.open) {
|
||||
if (event.type == api.NotificationEventType.open) {
|
||||
// The notification has been tapped
|
||||
sendEvent(
|
||||
MessageNotificationTappedEvent(
|
||||
@ -47,12 +63,12 @@ class NotificationsService {
|
||||
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();
|
||||
// Mark the message as read
|
||||
await GetIt.I.get<MessageService>().markMessageAsRead(
|
||||
event.extra![_messageIdKey]!,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
// [XmppService.sendReadMarker] will check whether the *SHOULD* send
|
||||
// the marker, i.e. if the privacy settings allow it.
|
||||
true,
|
||||
@ -83,7 +99,7 @@ class NotificationsService {
|
||||
|
||||
// Clear notifications
|
||||
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
|
||||
assert(
|
||||
event.payload != null,
|
||||
@ -93,7 +109,7 @@ class NotificationsService {
|
||||
final notification = modeln.Notification(
|
||||
event.id,
|
||||
conversationJid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@ -119,8 +135,8 @@ class NotificationsService {
|
||||
/// Configures the translatable strings on the native side
|
||||
/// using locale is currently configured.
|
||||
Future<void> configureNotificationI18n() async {
|
||||
await MoxplatformPlugin.notifications.setI18n(
|
||||
NotificationI18nData(
|
||||
await _api.setNotificationI18n(
|
||||
api.NotificationI18nData(
|
||||
reply: t.notifications.message.reply,
|
||||
markAsRead: t.notifications.message.markAsRead,
|
||||
you: t.messages.you,
|
||||
@ -129,32 +145,68 @@ class NotificationsService {
|
||||
}
|
||||
|
||||
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.
|
||||
await MoxplatformPlugin.notifications.createNotificationChannel(
|
||||
t.notifications.channels.messagesChannelName,
|
||||
t.notifications.channels.messagesChannelDescription,
|
||||
_messageChannelKey,
|
||||
true,
|
||||
);
|
||||
await MoxplatformPlugin.notifications.createNotificationChannel(
|
||||
t.notifications.channels.warningChannelName,
|
||||
t.notifications.channels.warningChannelDescription,
|
||||
_warningChannelKey,
|
||||
false,
|
||||
);
|
||||
await _api.createNotificationChannels([
|
||||
api.NotificationChannel(
|
||||
title: t.notifications.channels.messagesChannelName,
|
||||
description: t.notifications.channels.messagesChannelDescription,
|
||||
id: messageNotificationChannelId,
|
||||
importance: api.NotificationChannelImportance.HIGH,
|
||||
showBadge: true,
|
||||
vibration: true,
|
||||
enableLights: true,
|
||||
),
|
||||
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
|
||||
await configureNotificationI18n();
|
||||
|
||||
// Listen to notification events
|
||||
MoxplatformPlugin.notifications
|
||||
.getEventStream()
|
||||
.listen(onNotificationEvent);
|
||||
_eventStream.listen(onNotificationEvent);
|
||||
}
|
||||
|
||||
/// Returns true if a notification should be shown. false otherwise.
|
||||
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.
|
||||
@ -215,12 +267,28 @@ class NotificationsService {
|
||||
'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 senderTitle = c.isGroupchat
|
||||
? senderJid.resource
|
||||
: 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
|
||||
final newNotification = modeln.Notification(
|
||||
id,
|
||||
@ -230,8 +298,8 @@ class NotificationsService {
|
||||
senderJid.toString(),
|
||||
(avatarPath?.isEmpty ?? false) ? null : avatarPath,
|
||||
body,
|
||||
m.fileMetadata?.mimeType,
|
||||
m.fileMetadata?.path,
|
||||
fileMime,
|
||||
filePath,
|
||||
m.timestamp,
|
||||
);
|
||||
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],
|
||||
/// 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)?
|
||||
Future<void> updateNotification(
|
||||
Future<void> updateOrShowNotification(
|
||||
modelc.Conversation c,
|
||||
modelm.Message m,
|
||||
String accountJid,
|
||||
@ -263,37 +331,38 @@ class NotificationsService {
|
||||
}
|
||||
|
||||
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
|
||||
final notification = await _createNotification(
|
||||
c,
|
||||
m,
|
||||
accountJid,
|
||||
c.isGroupchat ? null : c.avatarPathWithOptionalContact,
|
||||
c.isGroupchat ? null : await c.avatarPathWithOptionalContactService,
|
||||
id,
|
||||
shouldOverride: true,
|
||||
);
|
||||
|
||||
await MoxplatformPlugin.notifications.showMessagingNotification(
|
||||
MessagingNotification(
|
||||
await _api.showMessagingNotification(
|
||||
api.MessagingNotification(
|
||||
title: await c.titleWithOptionalContactService,
|
||||
id: id,
|
||||
channelId: _messageChannelKey,
|
||||
channelId: messageNotificationChannelId,
|
||||
jid: c.jid,
|
||||
messages: [
|
||||
...notifications.map((n) {
|
||||
// Based on the table's composite primary key
|
||||
if (n.id == notification.id &&
|
||||
n.conversationJid == notification.conversationJid &&
|
||||
n.senderJid == notification.senderJid &&
|
||||
n.timestamp == notification.timestamp) {
|
||||
return notification.toNotificationMessage();
|
||||
}
|
||||
messages: notifications.map((n) {
|
||||
// Based on the table's composite primary key
|
||||
if (n.id == notification.id &&
|
||||
n.conversationJid == notification.conversationJid &&
|
||||
n.senderJid == notification.senderJid &&
|
||||
n.timestamp == notification.timestamp) {
|
||||
return notification.toNotificationMessage();
|
||||
}
|
||||
|
||||
return n.toNotificationMessage();
|
||||
}),
|
||||
],
|
||||
return n.toNotificationMessage();
|
||||
}).toList(),
|
||||
isGroupchat: c.isGroupchat,
|
||||
groupId: messageNotificationGroupId,
|
||||
extra: {
|
||||
_conversationJidKey: c.jid,
|
||||
_messageIdKey: m.id,
|
||||
@ -325,11 +394,11 @@ class NotificationsService {
|
||||
final id = notifications.isNotEmpty
|
||||
? notifications.first.id
|
||||
: Random().nextInt(_maxNotificationId);
|
||||
await MoxplatformPlugin.notifications.showMessagingNotification(
|
||||
MessagingNotification(
|
||||
await _api.showMessagingNotification(
|
||||
api.MessagingNotification(
|
||||
title: title,
|
||||
id: id,
|
||||
channelId: _messageChannelKey,
|
||||
channelId: messageNotificationChannelId,
|
||||
jid: c.jid,
|
||||
messages: [
|
||||
...notifications.map((n) => n.toNotificationMessage()),
|
||||
@ -344,6 +413,7 @@ class NotificationsService {
|
||||
.toNotificationMessage(),
|
||||
],
|
||||
isGroupchat: c.isGroupchat,
|
||||
groupId: messageNotificationGroupId,
|
||||
extra: {
|
||||
_conversationJidKey: c.jid,
|
||||
_messageIdKey: m.id,
|
||||
@ -364,13 +434,14 @@ class NotificationsService {
|
||||
return;
|
||||
}
|
||||
|
||||
await MoxplatformPlugin.notifications.showNotification(
|
||||
RegularNotification(
|
||||
await _api.showNotification(
|
||||
api.RegularNotification(
|
||||
title: title,
|
||||
body: body,
|
||||
channelId: _warningChannelKey,
|
||||
channelId: warningNotificationChannelId,
|
||||
id: Random().nextInt(_maxNotificationId),
|
||||
icon: NotificationIcon.warning,
|
||||
icon: api.NotificationIcon.warning,
|
||||
groupId: warningNotificationGroupId,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -401,14 +472,15 @@ class NotificationsService {
|
||||
final conversation = await GetIt.I
|
||||
.get<ConversationService>()
|
||||
.getConversationByJid(jid, accountJid);
|
||||
await MoxplatformPlugin.notifications.showNotification(
|
||||
RegularNotification(
|
||||
await _api.showNotification(
|
||||
api.RegularNotification(
|
||||
title: t.notifications.errors.messageError.title,
|
||||
body: t.notifications.errors.messageError
|
||||
.body(conversationTitle: conversation!.title),
|
||||
channelId: _warningChannelKey,
|
||||
channelId: warningNotificationChannelId,
|
||||
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 {
|
||||
final id = await _clearNotificationsForJid(jid, accountJid);
|
||||
if (id != null) {
|
||||
await MoxplatformPlugin.notifications.dismissNotification(id);
|
||||
await _api.dismissNotification(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -435,8 +507,7 @@ class NotificationsService {
|
||||
|
||||
// Dismiss the notification
|
||||
for (final idRaw in ids) {
|
||||
await MoxplatformPlugin.notifications
|
||||
.dismissNotification(idRaw['id']! as int);
|
||||
await _api.dismissNotification(idRaw['id']! as int);
|
||||
}
|
||||
|
||||
// Remove database entries
|
||||
@ -450,11 +521,10 @@ class NotificationsService {
|
||||
/// 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.
|
||||
Future<void> maybeSetAvatarFromState() async {
|
||||
final avatarPath =
|
||||
(await GetIt.I.get<XmppStateService>().getXmppState()).avatarUrl;
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final avatarPath = (await xss.state).avatarUrl;
|
||||
if (avatarPath.isNotEmpty) {
|
||||
await MoxplatformPlugin.notifications
|
||||
.setNotificationSelfAvatar(avatarPath);
|
||||
await _api.setNotificationSelfAvatar(avatarPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ class PermissionsService {
|
||||
/// `askedNotificationPermission` to true.
|
||||
Future<bool> shouldRequestNotificationPermission() async {
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final retValue = !(await xss.getXmppState()).askedNotificationPermission;
|
||||
final retValue = !(await xss.state).askedNotificationPermission;
|
||||
if (retValue) {
|
||||
await xss.modifyXmppState(
|
||||
(state) => state.copyWith(askedNotificationPermission: true),
|
||||
@ -29,8 +29,7 @@ class PermissionsService {
|
||||
}
|
||||
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final retValue =
|
||||
!(await xss.getXmppState()).askedBatteryOptimizationExcemption;
|
||||
final retValue = !(await xss.state).askedBatteryOptimizationExcemption;
|
||||
if (retValue) {
|
||||
await xss.modifyXmppState(
|
||||
(state) => state.copyWith(askedBatteryOptimizationExcemption: true),
|
||||
|
@ -135,6 +135,7 @@ class ReactionsService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
await GetIt.I.get<DatabaseService>().database.delete(
|
||||
reactionsTable,
|
||||
where:
|
||||
@ -143,7 +144,7 @@ class ReactionsService {
|
||||
id,
|
||||
accountJid,
|
||||
emoji,
|
||||
(await GetIt.I.get<XmppStateService>().getXmppState()).jid,
|
||||
(await xss.state).jid,
|
||||
],
|
||||
);
|
||||
final count = await _countReactions(id, accountJid, emoji);
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:logging/logging.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/cryptography/cryptography.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/files.dart';
|
||||
import 'package:moxxyv2/service/groupchat.dart';
|
||||
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
||||
import 'package:moxxyv2/service/language.dart';
|
||||
import 'package:moxxyv2/service/lifecycle.dart';
|
||||
import 'package:moxxyv2/service/message.dart';
|
||||
import 'package:moxxyv2/service/moxxmpp/connectivity.dart';
|
||||
import 'package:moxxyv2/service/moxxmpp/roster.dart';
|
||||
import 'package:moxxyv2/service/moxxmpp/socket.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/omemo/omemo.dart';
|
||||
import 'package:moxxyv2/service/permissions.dart';
|
||||
@ -70,10 +74,30 @@ Future<void> initializeServiceIfNeeded() async {
|
||||
);
|
||||
} else {
|
||||
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(
|
||||
entrypoint,
|
||||
receiveUIEvent,
|
||||
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.
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> entrypoint() async {
|
||||
Future<void> entrypoint(String initialLocale) async {
|
||||
setupLogging();
|
||||
setupBackgroundEventHandler();
|
||||
|
||||
@ -159,6 +183,9 @@ Future<void> entrypoint() async {
|
||||
GetIt.I.registerSingleton<DatabaseService>(DatabaseService());
|
||||
await GetIt.I.get<DatabaseService>().initialize();
|
||||
|
||||
// Initialize the account state
|
||||
await GetIt.I.get<XmppStateService>().initializeXmppState();
|
||||
|
||||
// Initialize services
|
||||
GetIt.I.registerSingleton<ConnectivityWatcherService>(
|
||||
ConnectivityWatcherService(),
|
||||
@ -182,9 +209,27 @@ Future<void> entrypoint() async {
|
||||
GetIt.I.registerSingleton<StorageService>(StorageService());
|
||||
GetIt.I.registerSingleton<ShareService>(ShareService());
|
||||
GetIt.I.registerSingleton<PermissionsService>(PermissionsService());
|
||||
GetIt.I.registerSingleton<LifecycleService>(LifecycleService());
|
||||
final xmpp = XmppService();
|
||||
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<ContactsService>().initialize();
|
||||
await GetIt.I.get<ConnectivityService>().initialize();
|
||||
@ -233,7 +278,7 @@ Future<void> entrypoint() async {
|
||||
(toJid, _) async =>
|
||||
GetIt.I.get<ConversationService>().shouldEncryptForConversation(
|
||||
toJid,
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
),
|
||||
),
|
||||
PingManager(const Duration(minutes: 3)),
|
||||
|
@ -155,7 +155,8 @@ JOIN
|
||||
);
|
||||
|
||||
// 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
|
||||
.get<moxxmpp.XmppConnection>()
|
||||
.getManagerById<moxxmpp.StickersManager>(moxxmpp.stickersManager)!
|
||||
@ -168,7 +169,8 @@ JOIN
|
||||
|
||||
Future<void> _publishStickerPack(moxxmpp.StickerPack pack) async {
|
||||
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
|
||||
.get<moxxmpp.XmppConnection>()
|
||||
.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/jobs.dart';
|
||||
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
||||
import 'package:moxxyv2/service/lifecycle.dart';
|
||||
import 'package:moxxyv2/service/message.dart';
|
||||
import 'package:moxxyv2/service/not_specified.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.
|
||||
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
|
||||
StreamSubscription<dynamic>? _xmppConnectionSubscription;
|
||||
|
||||
/// Stores whether the app is open or not. Useful for notifications.
|
||||
void setAppState(bool open) {
|
||||
_appOpen = open;
|
||||
}
|
||||
|
||||
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) {
|
||||
return null;
|
||||
@ -112,13 +106,11 @@ class XmppService {
|
||||
/// greater than 0.
|
||||
Future<void> setCurrentlyOpenedChatJid(String jid) async {
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
|
||||
_currentlyOpenedChatJid = jid;
|
||||
final cs = GetIt.I.get<ConversationService>()..activeConversationJid = jid;
|
||||
|
||||
final conversation = await cs.createOrUpdateConversation(
|
||||
jid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
update: (c) async {
|
||||
if (c.unreadCounter > 0) {
|
||||
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
|
||||
/// [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
|
||||
@ -475,7 +464,7 @@ class XmppService {
|
||||
bool triggeredFromUI,
|
||||
) async {
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final state = await xss.getXmppState();
|
||||
final state = await xss.state;
|
||||
final conn = GetIt.I.get<XmppConnection>();
|
||||
final lastResource = state.resource ?? '';
|
||||
|
||||
@ -503,8 +492,8 @@ class XmppService {
|
||||
bool triggeredFromUI,
|
||||
) async {
|
||||
final xss = GetIt.I.get<XmppStateService>();
|
||||
final state = await xss.getXmppState();
|
||||
final conn = GetIt.I.get<XmppConnection>();
|
||||
final state = await xss.state;
|
||||
final lastResource = state.resource ?? '';
|
||||
|
||||
_loginTriggeredFromUI = triggeredFromUI;
|
||||
@ -859,7 +848,7 @@ class XmppService {
|
||||
.requestRoster();
|
||||
|
||||
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 rosterItem = await rs.getRosterItemByJid(
|
||||
jid.toString(),
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
);
|
||||
if (rosterItem != null) {
|
||||
final pm = GetIt.I
|
||||
@ -941,7 +930,7 @@ class XmppService {
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
final dbMsg = await ms.getMessageByStanzaId(
|
||||
event.id,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
queryReactionPreview: false,
|
||||
);
|
||||
if (dbMsg == null) {
|
||||
@ -976,7 +965,7 @@ class XmppService {
|
||||
final sender = event.from.toBare().toString();
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
// 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) {
|
||||
_log.warning('Did not find the message in the database!');
|
||||
return;
|
||||
@ -1009,7 +998,7 @@ class XmppService {
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
final conversation = await cs.getConversationByJid(
|
||||
jid,
|
||||
await GetIt.I.get<XmppStateService>().getAccountJid(),
|
||||
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||
);
|
||||
if (conversation == null) return;
|
||||
|
||||
@ -1281,7 +1270,7 @@ class XmppService {
|
||||
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||
|
||||
if (event.type == 'error') {
|
||||
await _handleErrorMessage(event, accountJid);
|
||||
await _handleErrorMessage(event, accountJid!);
|
||||
_log.finest('Processed error message. Ending event processing here.');
|
||||
return;
|
||||
}
|
||||
@ -1294,7 +1283,7 @@ class XmppService {
|
||||
|
||||
// Process message corrections separately
|
||||
if (event.extensions.get<LastMessageCorrectionData>() != null) {
|
||||
await _handleMessageCorrection(event, conversationJid, accountJid);
|
||||
await _handleMessageCorrection(event, conversationJid, accountJid!);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1303,19 +1292,19 @@ class XmppService {
|
||||
await _handleFileUploadNotificationReplacement(
|
||||
event,
|
||||
conversationJid,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.extensions.get<MessageRetractionData>() != null) {
|
||||
await _handleMessageRetraction(event, conversationJid, accountJid);
|
||||
await _handleMessageRetraction(event, conversationJid, accountJid!);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle message reactions
|
||||
if (event.extensions.get<MessageReactionsData>() != null) {
|
||||
await _handleMessageReactions(event, conversationJid, accountJid);
|
||||
await _handleMessageReactions(event, conversationJid, accountJid!);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1332,12 +1321,12 @@ class XmppService {
|
||||
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();
|
||||
// The (portential) roster item of the chat partner
|
||||
final rosterItem = await GetIt.I
|
||||
.get<RosterService>()
|
||||
.getRosterItemByJid(conversationJid, accountJid);
|
||||
.getRosterItemByJid(conversationJid, accountJid!);
|
||||
// Is the conversation partner in our roster
|
||||
final isInRoster = rosterItem != null;
|
||||
// True if the message was sent by us (via a Carbon)
|
||||
@ -1524,7 +1513,7 @@ class XmppService {
|
||||
? mimeTypeToEmoji(mimeGuess)
|
||||
: messageBody;
|
||||
// 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
|
||||
var isMuted = false;
|
||||
// Whether to send the notification
|
||||
@ -1588,7 +1577,8 @@ class XmppService {
|
||||
isMuted = c != null ? c.muted : prefs.defaultMuteState;
|
||||
sendNotification = !sent &&
|
||||
shouldNotify &&
|
||||
(!isConversationOpened || !_appOpen) &&
|
||||
(!isConversationOpened ||
|
||||
!GetIt.I.get<LifecycleService>().isActive) &&
|
||||
!isMuted;
|
||||
},
|
||||
);
|
||||
@ -1730,7 +1720,7 @@ class XmppService {
|
||||
final ms = GetIt.I.get<MessageService>();
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
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) {
|
||||
// Ack the message
|
||||
final newMsg = await ms.updateMessage(
|
||||
@ -1790,7 +1780,7 @@ class XmppService {
|
||||
final ms = GetIt.I.get<MessageService>();
|
||||
final message = await ms.getMessageByStanzaId(
|
||||
event.data.stanza.id!,
|
||||
accountJid,
|
||||
accountJid!,
|
||||
);
|
||||
|
||||
if (message == null) {
|
||||
|
@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxyv2/service/database/constants.dart';
|
||||
import 'package:moxxyv2/service/database/database.dart';
|
||||
import 'package:moxxyv2/service/xmpp.dart';
|
||||
import 'package:moxxyv2/shared/models/xmpp_state.dart';
|
||||
import 'package:random_string/random_string.dart';
|
||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
@ -30,7 +31,9 @@ class XmppStateService {
|
||||
final Logger _log = Logger('XmppStateService');
|
||||
|
||||
/// 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.
|
||||
String? _accountJid;
|
||||
@ -88,6 +91,7 @@ class XmppStateService {
|
||||
await db.insert(
|
||||
xmppStateTable,
|
||||
{
|
||||
'accountJid': _accountJid,
|
||||
'key': _userAgentKey,
|
||||
'value': jsonEncode(_userAgent!.toJson()),
|
||||
},
|
||||
@ -111,32 +115,48 @@ class XmppStateService {
|
||||
});
|
||||
}
|
||||
|
||||
Future<XmppState> getXmppState() async {
|
||||
if (_state != null) return _state!;
|
||||
Future<void> initializeXmppState() async {
|
||||
// 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 rowsRaw = await GetIt.I.get<DatabaseService>().database.query(
|
||||
xmppStateTable,
|
||||
where: 'accountJid = ?',
|
||||
whereArgs: [await getAccountJid()],
|
||||
whereArgs: [accountJid],
|
||||
columns: ['key', 'value'],
|
||||
);
|
||||
if (rowsRaw.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final row in rowsRaw) {
|
||||
json[row['key']! as String] = row['value'] as String?;
|
||||
}
|
||||
|
||||
_log.finest(json);
|
||||
_state = XmppState.fromDatabaseTuples(json);
|
||||
return _state!;
|
||||
return XmppState.fromDatabaseTuples(json);
|
||||
}
|
||||
|
||||
/// A wrapper to modify the [XmppState] and commit it.
|
||||
Future<void> modifyXmppState(XmppState Function(XmppState) func) async {
|
||||
_state = func(_state!);
|
||||
|
||||
final accountJid = await getAccountJid();
|
||||
/// The same as [commitXmppState] but without aquiring [_stateLock].
|
||||
Future<void> _commitXmppState(String accountJid) async {
|
||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
||||
for (final tuple in _state!.toDatabaseTuples().entries) {
|
||||
for (final tuple in _state.toDatabaseTuples().entries) {
|
||||
batch.insert(
|
||||
xmppStateTable,
|
||||
<String, String?>{
|
||||
@ -150,6 +170,43 @@ class XmppStateService {
|
||||
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.
|
||||
Future<void> resetAccountJid() async {
|
||||
_accountJid = null;
|
||||
@ -157,26 +214,29 @@ class XmppStateService {
|
||||
}
|
||||
|
||||
/// 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;
|
||||
await _storage.write(key: _accountJidKey, value: jid);
|
||||
|
||||
if (commit) {
|
||||
await _storage.write(key: _accountJidKey, value: jid);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _loadAccountJid() async {
|
||||
return _accountJid ??= await _storage.read(key: _accountJidKey);
|
||||
}
|
||||
|
||||
/// Returns a string if we have an account jid and null if we don't.
|
||||
Future<String?> getRawAccountJid() async {
|
||||
if (_accountJid != null) {
|
||||
return _accountJid;
|
||||
/// Gets the current account JID from the cache or from the secure storage.
|
||||
Future<String?> getAccountJid() async {
|
||||
return _accountJid ?? await _loadAccountJid();
|
||||
}
|
||||
|
||||
Future<bool> isLoggedIn(String? accountJid) async {
|
||||
final s = await state;
|
||||
if (accountJid == null || s.jid == null || s.password == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _loadAccountJid();
|
||||
}
|
||||
|
||||
/// Gets the current account JID from the cache or from the secure storage.
|
||||
Future<String> getAccountJid() async {
|
||||
return _accountJid ?? (await _loadAccountJid())!;
|
||||
return await GetIt.I.get<XmppService>().getConnectionSettings() != null;
|
||||
}
|
||||
}
|
||||
|
@ -23,4 +23,14 @@ const maxStickerPackPages = 2;
|
||||
|
||||
/// An "invalid" fake JID to make share_handler happy when adding the self-chat
|
||||
/// 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_provider/path_provider.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
|
||||
/// 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
|
||||
/// to the JID of the conversation the file comes from.
|
||||
/// If the thumbnail already exists, then just its path is returned. If not, then
|
||||
/// it gets generated first.
|
||||
Future<String?> getVideoThumbnailPath(
|
||||
String path,
|
||||
String conversationJid,
|
||||
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;
|
||||
/// Returns true if we can generate a video thumbnail of mime type [mime]. If not, returns
|
||||
/// false.
|
||||
bool canGenerateVideoThumbnail(String mime) {
|
||||
return ![
|
||||
// Ignore mime types that may be wacky
|
||||
'video/webm',
|
||||
].contains(mime);
|
||||
}
|
||||
|
||||
Future<String> getContactProfilePicturePath(String id) async {
|
||||
|
@ -62,6 +62,23 @@ enum ConversationType {
|
||||
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
|
||||
|
@ -1,5 +1,5 @@
|
||||
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.g.dart';
|
||||
@ -44,12 +44,12 @@ class Notification with _$Notification {
|
||||
factory Notification.fromJson(Map<String, dynamic> json) =>
|
||||
_$NotificationFromJson(json);
|
||||
|
||||
NotificationMessage toNotificationMessage() {
|
||||
return NotificationMessage(
|
||||
api.NotificationMessage toNotificationMessage() {
|
||||
return api.NotificationMessage(
|
||||
sender: sender,
|
||||
jid: senderJid,
|
||||
avatarPath: avatarPath,
|
||||
content: NotificationMessageContent(
|
||||
content: api.NotificationMessageContent(
|
||||
body: body,
|
||||
mime: mime,
|
||||
path: path,
|
||||
|
@ -97,4 +97,6 @@ class XmppState with _$XmppState {
|
||||
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<ConversationMarkedAsReadEvent>(_onConversationMarkedAsRead);
|
||||
on<ConversationsSetEvent>(_onConversationsSet);
|
||||
on<ConversationExitedEvent>(_onConversationExited);
|
||||
}
|
||||
|
||||
// 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(
|
||||
ConversationsAddedEvent event,
|
||||
Emitter<ConversationsState> emit,
|
||||
|
@ -16,6 +16,14 @@ class ConversationsInitEvent extends ConversationsEvent {
|
||||
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.
|
||||
class ConversationsAddedEvent extends ConversationsEvent {
|
||||
ConversationsAddedEvent(this.conversation);
|
||||
|
@ -7,6 +7,7 @@ import 'package:moxxyv2/shared/events.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversations_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/service/data.dart';
|
||||
|
||||
@ -106,6 +107,16 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
|
||||
(_) => false,
|
||||
),
|
||||
);
|
||||
GetIt.I.get<RequestBloc>().add(
|
||||
RequestsSetEvent(
|
||||
[
|
||||
if (result.preStart.requestNotificationPermission)
|
||||
Request.notifications,
|
||||
if (result.preStart.excludeFromBatteryOptimisation)
|
||||
Request.batterySavingExcemption,
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (result is LoginFailureEvent) {
|
||||
GetIt.I.get<UIDataService>().isLoggedIn = false;
|
||||
return emit(
|
||||
|
@ -568,9 +568,6 @@ class BidirectionalConversationController
|
||||
_textController.dispose();
|
||||
_audioRecorder.dispose();
|
||||
|
||||
// Tell the contact that we're gone
|
||||
_updateChatState(ChatState.gone);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||
import 'package:moxxyv2/shared/models/message.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/helpers.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/blink.dart';
|
||||
@ -373,6 +374,13 @@ class ConversationPageState extends State<ConversationPage>
|
||||
|
||||
// Clear the read marker cache
|
||||
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;
|
||||
},
|
||||
child: KeyboardReplacerScaffold(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/ui/bloc/request_bloc.dart';
|
||||
@ -39,18 +40,18 @@ class RequestDialog extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
switch (request) {
|
||||
case Request.notifications:
|
||||
Permission.notification.request();
|
||||
await Permission.notification.request();
|
||||
break;
|
||||
case Request.batterySavingExcemption:
|
||||
MoxplatformPlugin.platform
|
||||
await MoxplatformPlugin.platform
|
||||
.openBatteryOptimisationSettings();
|
||||
break;
|
||||
}
|
||||
|
||||
context.read<RequestBloc>().add(NextRequestEvent());
|
||||
GetIt.I.get<RequestBloc>().add(NextRequestEvent());
|
||||
},
|
||||
child: Text(t.permissions.allow),
|
||||
),
|
||||
|
@ -1,8 +1,15 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/thumbnails/helpers.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 {
|
||||
const VideoThumbnail({
|
||||
required this.path,
|
||||
@ -21,7 +28,7 @@ class VideoThumbnail extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String?>(
|
||||
future: getVideoThumbnailPath(path, conversationJid, mime),
|
||||
future: _videoThumbnailWrapper(path, mime),
|
||||
builder: (context, snapshot) {
|
||||
Widget widget;
|
||||
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
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "8745ddb5f27423c6ba4cc3b182688407239fe38f73ef93a0db0a3497ddf4c2e6"
|
||||
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "46.0.0"
|
||||
version: "61.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "2c93c461a00a27dad2849137304d32b3c6b79c75b1d10ec9547ce329de329524"
|
||||
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
version: "5.13.0"
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -57,14 +57,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -101,18 +93,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "29a03af98de60b4eb9136acd56608a54e989f6da238a80af739415b05589d6df"
|
||||
sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: ad77deb6e9c143a3f550fbb4c5c1e0c6aadabe24274898d06b9526c61b9cf4fb
|
||||
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.1.1"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -125,18 +117,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "9aae031a54ab0beebc30a888c93e900d15ae2fd8883d031dbfbd5ebdb57f5a4c"
|
||||
sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
version: "2.2.1"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "361d73f37cd48c47a81a61421eb1cc4cfd2324516fbb52f1bc4c9a01834ef2de"
|
||||
sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
version: "2.3.3"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -365,10 +357,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "8aff82f9b26fd868992e5430335a9d773bfef01e1d852d7ba71bf4c5d9349351"
|
||||
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.2"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -652,10 +644,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: b8f1017d491344ef41045d3fe85950404c49e74eeaf84a84d7b8b67ac24dfb91
|
||||
sha256: "20db669e3663fd0cbfb53729e513199fb08627ae40de0db85e0b0fe32f82bf82"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0+1"
|
||||
version: "2.1.1"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -668,10 +660,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "4f4a162323c86ffc1245765cfe138872b8f069deb42f7dbb36115fa27f31469b"
|
||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "3.2.0"
|
||||
get_it:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -780,10 +772,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: "0cec7060459254cf1ff980c08dedca6fa50917724a3c3ec8c5026cb88dee8238"
|
||||
sha256: fd1bcfbf6f623e1dfcc60616f189a6ca540dba7b5917447be5dab754b3116932
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
keyboard_height_plugin:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -893,26 +885,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: moxplatform
|
||||
sha256: "68d96d411ce0c8ddf7f60a89b338edb88f4a9a5405545d925d1fbb784402f16a"
|
||||
sha256: "02ac0ee17f30e2bc3914a3f177d97f02d9fd062bc69ad4d5dd17404d2829f2c7"
|
||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||
source: hosted
|
||||
version: "0.1.17+4"
|
||||
version: "0.1.17+6"
|
||||
moxplatform_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: moxplatform_android
|
||||
sha256: a0683b90030c5e4cd163dccf96c61a3aa513ace664a2c9a581d7970fbd683251
|
||||
sha256: "6b46109ec5d2c4a43cc68ac3dcf0396e5237c5800671a7e2dbcd22f468a13e28"
|
||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||
source: hosted
|
||||
version: "0.1.20"
|
||||
version: "0.1.22"
|
||||
moxplatform_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: moxplatform_platform_interface
|
||||
sha256: "14143f2850673fcb56535380718aec7d7d1f817986851a29fd83a905c1ea94fd"
|
||||
sha256: "27a6691d6913a291f7bc93d98c87eef491a4e47a76d3d804d1e0a2de7a5875f0"
|
||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||
source: hosted
|
||||
version: "0.1.20"
|
||||
version: "0.1.22"
|
||||
moxxmpp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -933,10 +925,9 @@ packages:
|
||||
moxxyv2_builders:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: moxxyv2_builders
|
||||
sha256: f936974cfc6f80308ecd348889e365feed164887cb576caf8c82acadfa7cc0c9
|
||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||
source: hosted
|
||||
path: "../moxxyv2_builders"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.2.0"
|
||||
native_imaging:
|
||||
dependency: "direct main"
|
||||
@ -1122,6 +1113,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
pigeon:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pigeon
|
||||
sha256: "5a79fd0b10423f6b5705525e32015597f861c31220b522a67d1e6b580da96719"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.1"
|
||||
pinenacl:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1451,18 +1450,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "00f8b6b586f724a8c769c96f1d517511a41661c0aede644544d8d86a1ab11142"
|
||||
sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.3.2"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "522d9b05c40ec14f479ce4428337d106c0465fedab42f514582c198460a784fe"
|
||||
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.4"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1712,14 +1711,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1788,10 +1779,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace"
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
flutter: ">=3.7.3"
|
||||
|
19
pubspec.yaml
19
pubspec.yaml
@ -13,7 +13,6 @@ dependencies:
|
||||
archive: 3.3.2
|
||||
audiofileplayer: 2.1.1
|
||||
auto_size_text: 3.0.0
|
||||
awesome_notifications: 0.7.4+1
|
||||
badges: 2.0.3
|
||||
better_open_file: 3.6.3
|
||||
bloc: 8.1.0
|
||||
@ -65,7 +64,7 @@ dependencies:
|
||||
version: ^0.2.0
|
||||
moxplatform:
|
||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||
version: 0.1.17+4
|
||||
version: 0.1.17+6
|
||||
moxxmpp:
|
||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||
version: 0.4.0
|
||||
@ -103,11 +102,10 @@ dependencies:
|
||||
url_launcher: 6.1.5
|
||||
#unifiedpush: 3.0.1
|
||||
uuid: 3.0.5
|
||||
video_thumbnail: 0.5.3
|
||||
visibility_detector: 0.4.0+2
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.1.11
|
||||
build_runner: ^2.3.3
|
||||
flutter_launcher_icons: ^0.9.3
|
||||
flutter_lints: ^2.0.1
|
||||
#flutter_test:
|
||||
@ -116,6 +114,7 @@ dev_dependencies:
|
||||
#integration_test:
|
||||
# sdk: flutter
|
||||
json_serializable: ^6.3.1
|
||||
pigeon: 11.0.1
|
||||
slang_build_runner: 3.4.0
|
||||
test: ^1.21.1
|
||||
very_good_analysis: ^4.0.0
|
||||
@ -133,14 +132,21 @@ dependency_overrides:
|
||||
# path: ../moxxmpp/packages/moxxmpp_socket_tcp
|
||||
# omemo_dart:
|
||||
# path: ../../Personal/omemo_dart
|
||||
# moxplatform:
|
||||
# path: ../moxplatform/packages/moxplatform
|
||||
# moxplatform_android:
|
||||
# path: ../moxplatform/packages/moxplatform_android
|
||||
# moxplatform_platform_interface:
|
||||
# path: ../moxplatform/packages/moxplatform_platform_interface
|
||||
|
||||
moxxmpp:
|
||||
git:
|
||||
url: https://codeberg.org/moxxy/moxxmpp.git
|
||||
rev: 864cc0e7474d98f691d87b7a0806f428bf98b790
|
||||
path: packages/moxxmpp
|
||||
|
||||
moxxyv2_builders:
|
||||
path: ../moxxyv2_builders
|
||||
|
||||
extra_licenses:
|
||||
- name: undraw.co
|
||||
@ -152,6 +158,11 @@ extra_licenses:
|
||||
url: "https://invent.kde.org/melvo/xmpp-providers"
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: org.moxxy.moxxyv2
|
||||
pluginClass: MainActivity
|
||||
uses-material-design: true
|
||||
fonts:
|
||||
- family: RobotoMono
|
||||
|
@ -14,20 +14,21 @@ void main() {
|
||||
Logger('TestLogger'),
|
||||
1,
|
||||
[
|
||||
DatabaseMigration(4, (_) async {
|
||||
Migration(4, (_) async {
|
||||
expect(counter, 3);
|
||||
counter++;
|
||||
}),
|
||||
DatabaseMigration(2, (_) async {
|
||||
Migration(2, (_) async {
|
||||
expect(counter, 1);
|
||||
counter++;
|
||||
}),
|
||||
DatabaseMigration(3, (_) async {
|
||||
Migration(3, (_) async {
|
||||
expect(counter, 2);
|
||||
counter++;
|
||||
}),
|
||||
],
|
||||
1,
|
||||
'test',
|
||||
);
|
||||
});
|
||||
|
||||
@ -37,21 +38,63 @@ void main() {
|
||||
Logger('TestLogger'),
|
||||
1,
|
||||
[
|
||||
DatabaseMigration(4, (_) async {
|
||||
Migration(4, (_) async {
|
||||
expect(counter, 3);
|
||||
counter++;
|
||||
}),
|
||||
DatabaseMigration(2, (_) async {
|
||||
Migration(2, (_) async {
|
||||
// This must never be called
|
||||
expect(true, false);
|
||||
}),
|
||||
DatabaseMigration(3, (_) async {
|
||||
Migration(3, (_) async {
|
||||
expect(counter, 2);
|
||||
counter++;
|
||||
}),
|
||||
],
|
||||
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