diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json index 65039688..eb29ed08 100644 --- a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 25, - "identityHash": "ee8ddca7a73aef753951c2e2522cbb28", + "identityHash": "7ab8482b8d5dcb97c4c8932f578879f2", "entities": [ { "tableName": "TootEntity", @@ -92,7 +92,7 @@ }, { "tableName": "AccountEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", "fields": [ { "fieldPath": "id", @@ -190,6 +190,12 @@ "affinity": "INTEGER", "notNull": true }, + { + "fieldPath": "notificationsChatMessages", + "columnName": "notificationsChatMessages", + "affinity": "INTEGER", + "notNull": true + }, { "fieldPath": "notificationSound", "columnName": "notificationSound", @@ -873,7 +879,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee8ddca7a73aef753951c2e2522cbb28')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7ab8482b8d5dcb97c4c8932f578879f2')" ] } } \ No newline at end of file diff --git a/app/src/husky/res/values-ru/strings.xml b/app/src/husky/res/values-ru/strings.xml index bc29c985..fe07411a 100644 --- a/app/src/husky/res/values-ru/strings.xml +++ b/app/src/husky/res/values-ru/strings.xml @@ -19,7 +19,12 @@ %s среагировал с %s на ваш пост Эмодзи реакции Уведомления о новых эмодзи реакциях + %s отправил вам сообщение + Сообщения + Уведомления о новых сообщениях Синтаксис форматирования по умолчанию(если поддерживается) на мои посты отреагировали + получено новое сообщение Скрывать заглушенных пользователей + Ссылка diff --git a/app/src/husky/res/values/strings.xml b/app/src/husky/res/values/strings.xml index 66d9ea72..56ad4784 100644 --- a/app/src/husky/res/values/strings.xml +++ b/app/src/husky/res/values/strings.xml @@ -26,9 +26,13 @@ %s reacted with %s to your post Emoji Reactions Notifications about new emoji reactions + %s sent you a message + Chat Messages + Notifications about new chat messages Default formatting syntax(if supported by instance) my posts are reacted with emojis + received a chat message Hide muted users Enable bigger custom emojis Enable experimental Pleroma-FE stickers(if available) @@ -37,6 +41,8 @@ Video Audio Attachment + + Link Post visibility diff --git a/app/src/main/ic_bbcode.svg b/app/src/main/ic_bbcode.svg new file mode 100644 index 00000000..a24790d8 --- /dev/null +++ b/app/src/main/ic_bbcode.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/src/main/ic_html.svg b/app/src/main/ic_html.svg new file mode 100644 index 00000000..30224091 --- /dev/null +++ b/app/src/main/ic_html.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/src/main/ic_sticker.svg b/app/src/main/ic_sticker.svg new file mode 100644 index 00000000..11325219 --- /dev/null +++ b/app/src/main/ic_sticker.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 9598edeb..2149cb93 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -54,6 +54,7 @@ import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.ActionButtonActivity @@ -74,6 +75,10 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_main.* +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException import javax.inject.Inject class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { @@ -187,6 +192,51 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje // Setup push notifications if (NotificationHelper.areNotificationsEnabled(this, accountManager)) { NotificationHelper.enablePullNotifications(this) + + // Use when WorkManager doesn't want to work +/* + val accountList = accountManager.getAllAccountsOrderedByActive() + for (account in accountList) { + if (account.notificationsEnabled) { + try { + Log.d(TAG, "getting Notifications for " + account.fullName) + // don't care about withMuted because they are always silently ignored + val notificationsResponse = mastodonApi.notificationsWithAuth( + String.format("Bearer %s", account.accessToken), + account.domain, true, + setOf(Notification.Type.CHAT_MESSAGE.presentation) + ).enqueue(object: Callback> { + override fun onFailure(call: Call>, t: Throwable) { + + } + + override fun onResponse(call: Call>, response: Response>) { + val notifications = response.body() + val newId = account.lastNotificationId + var newestId = "" + var isFirstOfBatch = true + notifications?.reversed()?.forEach { notification -> + val currentId = notification.id + if (newestId.isLessThan(currentId)) { + newestId = currentId + } + if (newId.isLessThan(currentId)) { + NotificationHelper.make(this@MainActivity, notification, account, isFirstOfBatch) + isFirstOfBatch = false + } + } + account.lastNotificationId = newestId + accountManager.saveAccount(account) + } + }) + } catch (e: IOException) { + Log.w(TAG, "error receiving notifications", e) + } + } + + */ + } + } else { NotificationHelper.disablePullNotifications(this) } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt index d76d06ae..8fb1aa85 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt @@ -82,6 +82,10 @@ class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) { content.setTypeface(null, Typeface.ITALIC) content.resources.getString(it.attachment.describeAttachmentType()) + } else if (it.card != null) { + content.setTypeface(null, Typeface.ITALIC) + + content.resources.getString(R.string.link) } else "" content.text = if(it.accountId == localUserId) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 0e66ae4f..24806409 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -52,6 +52,7 @@ import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.entity.ChatMessage; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.PollOption; @@ -90,6 +91,8 @@ public class NotificationHelper { public static final String COMPOSE_ACTION = "COMPOSE_ACTION"; + public static final String CHAT_REPLY_ACTION = "CHAT_REPLY_ACTION"; + public static final String KEY_REPLY = "KEY_REPLY"; public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID"; @@ -112,6 +115,8 @@ public class NotificationHelper { public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL"; + public static final String KEY_CHAT_ID = "KEY_CHAT_ID"; + /** * notification channels used on Android O+ **/ @@ -122,6 +127,7 @@ public class NotificationHelper { public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; public static final String CHANNEL_POLL = "CHANNEL_POLL"; public static final String CHANNEL_EMOJI_REACTION = "CHANNEL_EMOJI_REACTION"; + public static final String CHANNEL_CHAT_MESSAGES = "CHANNEL_CHAT_MESSAGES"; /** @@ -151,13 +157,13 @@ public class NotificationHelper { } // Pleroma extension: don't notify about seen notifications - if (body.getPleroma() != null && body.getPleroma().getSeen() == true) { + if (body.getPleroma() != null && body.getPleroma().getSeen()) { return; } if (body.getStatus() != null && - (body.getStatus().isUserMuted() == true || - body.getStatus().isThreadMuted() == true)) { + (body.getStatus().isUserMuted() || + body.getStatus().isThreadMuted())) { return; } @@ -218,30 +224,45 @@ public class NotificationHelper { builder.setLargeIcon(accountAvatar); // Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are available from Nougat - if (body.getType() == Notification.Type.MENTION - && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) - .setLabel(context.getString(R.string.label_quick_reply)) - .build(); + if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if(body.getType() == Notification.Type.MENTION) { + RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) + .setLabel(context.getString(R.string.label_quick_reply)) + .build(); + + PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account); + + NotificationCompat.Action quickReplyAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_quick_reply), quickReplyPendingIntent) + .addRemoteInput(replyRemoteInput) + .build(); - PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account); + builder.addAction(quickReplyAction); - NotificationCompat.Action quickReplyAction = - new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, - context.getString(R.string.action_quick_reply), quickReplyPendingIntent) - .addRemoteInput(replyRemoteInput) - .build(); + PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account); - builder.addAction(quickReplyAction); + NotificationCompat.Action composeAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_compose_shortcut), composePendingIntent) + .build(); - PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account); + builder.addAction(composeAction); + } else if(body.getType() == Notification.Type.CHAT_MESSAGE) { + RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) + .setLabel(context.getString(R.string.label_quick_reply)) + .build(); - NotificationCompat.Action composeAction = - new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, - context.getString(R.string.action_compose_shortcut), composePendingIntent) - .build(); + PendingIntent quickReplyPendingIntent = getStatusReplyIntent(CHAT_REPLY_ACTION, context, body, account); - builder.addAction(composeAction); + NotificationCompat.Action quickReplyAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_quick_reply), quickReplyPendingIntent) + .addRemoteInput(replyRemoteInput) + .build(); + + builder.addAction(quickReplyAction); + } } builder.setSubText(account.getFullName()); @@ -326,35 +347,40 @@ public class NotificationHelper { } private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) { - Status status = body.getStatus(); - - String citedLocalAuthor = status.getAccount().getLocalUsername(); - String citedText = status.getContent().toString(); - String inReplyToId = status.getId(); - Status actionableStatus = status.getActionableStatus(); - Status.Visibility replyVisibility = actionableStatus.getVisibility(); - String contentWarning = actionableStatus.getSpoilerText(); - Status.Mention[] mentions = actionableStatus.getMentions(); - List mentionedUsernames = new ArrayList<>(); - mentionedUsernames.add(actionableStatus.getAccount().getUsername()); - for (Status.Mention mention : mentions) { - mentionedUsernames.add(mention.getUsername()); - } - mentionedUsernames.removeAll(Collections.singleton(account.getUsername())); - mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames)); - Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class) - .setAction(action) - .putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor) - .putExtra(KEY_CITED_TEXT, citedText) - .putExtra(KEY_SENDER_ACCOUNT_ID, account.getId()) - .putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier()) - .putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName()) - .putExtra(KEY_NOTIFICATION_ID, notificationId) - .putExtra(KEY_CITED_STATUS_ID, inReplyToId) - .putExtra(KEY_VISIBILITY, replyVisibility) - .putExtra(KEY_SPOILER, contentWarning) - .putExtra(KEY_MENTIONS, mentionedUsernames.toArray(new String[0])); + .setAction(action) + .putExtra(KEY_SENDER_ACCOUNT_ID, account.getId()) + .putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier()) + .putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName()) + .putExtra(KEY_NOTIFICATION_ID, notificationId); + + if(action == CHAT_REPLY_ACTION) { + replyIntent.putExtra(KEY_CHAT_ID, body.getChatMessage().getChatId()); + } else { + Status status = body.getStatus(); + + String citedLocalAuthor = status.getAccount().getLocalUsername(); + String citedText = status.getContent().toString(); + String inReplyToId = status.getId(); + Status actionableStatus = status.getActionableStatus(); + Status.Visibility replyVisibility = actionableStatus.getVisibility(); + String contentWarning = actionableStatus.getSpoilerText(); + Status.Mention[] mentions = actionableStatus.getMentions(); + List mentionedUsernames = new ArrayList<>(); + mentionedUsernames.add(actionableStatus.getAccount().getUsername()); + for (Status.Mention mention : mentions) { + mentionedUsernames.add(mention.getUsername()); + } + mentionedUsernames.removeAll(Collections.singleton(account.getUsername())); + mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames)); + + replyIntent.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor) + .putExtra(KEY_CITED_TEXT, citedText) + .putExtra(KEY_CITED_STATUS_ID, inReplyToId) + .putExtra(KEY_VISIBILITY, replyVisibility) + .putExtra(KEY_SPOILER, contentWarning) + .putExtra(KEY_MENTIONS, mentionedUsernames.toArray(new String[0])); + } return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, @@ -374,7 +400,8 @@ public class NotificationHelper { CHANNEL_BOOST + account.getIdentifier(), CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(), - CHANNEL_EMOJI_REACTION + account.getIdentifier() + CHANNEL_EMOJI_REACTION + account.getIdentifier(), + CHANNEL_CHAT_MESSAGES + account.getIdentifier() }; int[] channelNames = { R.string.notification_mention_name, @@ -384,6 +411,7 @@ public class NotificationHelper { R.string.notification_favourite_name, R.string.notification_poll_name, R.string.notification_emoji_name, + R.string.notification_chat_message_name, }; int[] channelDescriptions = { R.string.notification_mention_descriptions, @@ -392,7 +420,8 @@ public class NotificationHelper { R.string.notification_boost_description, R.string.notification_favourite_description, R.string.notification_poll_description, - R.string.notification_emoji_description + R.string.notification_emoji_description, + R.string.notification_chat_message_description, }; List channels = new ArrayList<>(6); @@ -489,7 +518,7 @@ public class NotificationHelper { PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS ) .addTag(NOTIFICATION_PULL_TAG) - .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + //.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) .build(); workManager.enqueue(workRequest); @@ -550,6 +579,8 @@ public class NotificationHelper { return account.getNotificationsPolls(); case EMOJI_REACTION: return account.getNotificationsEmojiReactions(); + case CHAT_MESSAGE: + return account.getNotificationsChatMessages(); default: return false; } @@ -572,6 +603,8 @@ public class NotificationHelper { return CHANNEL_POLL + account.getIdentifier(); case EMOJI_REACTION: return CHANNEL_EMOJI_REACTION + account.getIdentifier(); + case CHAT_MESSAGE: + return CHANNEL_CHAT_MESSAGES + account.getIdentifier(); default: return null; } @@ -653,6 +686,9 @@ public class NotificationHelper { } else { return context.getString(R.string.poll_ended_voted); } + case CHAT_MESSAGE: + return String.format(context.getString(R.string.notification_chat_message_format), + accountName); } return null; } @@ -686,6 +722,16 @@ public class NotificationHelper { } return builder.toString(); } + case CHAT_MESSAGE: + if (!TextUtils.isEmpty(notification.getChatMessage().getContent())) { + return notification.getChatMessage().getContent().toString(); + } else if(notification.getChatMessage().getAttachment() != null) { + return context.getString(notification.getChatMessage().getAttachment().describeAttachmentType()); + } else if(notification.getChatMessage().getCard() != null) { + return context.getString(R.string.link); + } else { + return ""; + } } return null; } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt index f4f2637e..7a38e8d1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt @@ -45,7 +45,8 @@ class NotificationWorker( // don't care about withMuted because they are always silently ignored val notificationsResponse = mastodonApi.notificationsWithAuth( String.format("Bearer %s", account.accessToken), - account.domain, true + account.domain, true, + setOf(Notification.Type.CHAT_MESSAGE.presentation) ).execute() val notifications = notificationsResponse.body() if (notificationsResponse.isSuccessful && notifications != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index 23a4d7fc..4b5ed2d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -44,6 +44,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, var notificationsFavorited: Boolean = true, var notificationsPolls: Boolean = true, var notificationsEmojiReactions: Boolean = true, + var notificationsChatMessages: Boolean = true, var notificationSound: Boolean = true, var notificationVibration: Boolean = true, var notificationLight: Boolean = true, diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index d0910ee5..878cac2e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -347,7 +347,7 @@ public abstract class AppDatabase extends RoomDatabase { public static final Migration MIGRATION_23_24 = new Migration(23, 24) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 1"); } }; @@ -371,6 +371,7 @@ public abstract class AppDatabase extends RoomDatabase { "`emojis` TEXT NOT NULL," + "PRIMARY KEY (`localId`, `messageId`))"); database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `chatLimit` INTEGER"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsChatMessages` INTEGER NOT NULL DEFAULT 1"); } }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt index c4761718..8b0e7fc7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt @@ -8,7 +8,6 @@ import io.reactivex.Single @Dao abstract class ChatsDao { - // TODO: must be ordering by date but it leads to issues @Query("""SELECT c.chatId, c.localId, c.accountId, c.lastMessageId, c.unread, c.updatedAt, a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', a.localUsername as 'a_localUsername', a.username as 'a_username', diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 64611d45..6d81b2ff 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -60,9 +60,9 @@ class NetworkModule { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { addInterceptor(HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BASIC + //level = HttpLoggingInterceptor.Level.BASIC //level = HttpLoggingInterceptor.Level.HEADERS - //level = HttpLoggingInterceptor.Level.BODY + level = HttpLoggingInterceptor.Level.BODY }) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index 2fbf4d4b..12e1a374 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.entity import com.google.gson.* import com.google.gson.annotations.SerializedName import com.google.gson.annotations.JsonAdapter +import java.util.* data class PleromaNotification( @SerializedName("is_seen") val seen: Boolean @@ -29,7 +30,9 @@ data class Notification( val account: Account, val status: Status?, val pleroma: PleromaNotification? = null, - val emoji: String? = null) { + val emoji: String? = null, + @SerializedName("chat_message") val chatMessage: ChatMessage? = null, + @SerializedName("created_at") val createdAt: Date? = null ) { @JsonAdapter(NotificationTypeAdapter::class) enum class Type(val presentation: String) { @@ -40,7 +43,8 @@ data class Notification( FOLLOW("follow"), POLL("poll"), EMOJI_REACTION("pleroma:emoji_reaction"), - FOLLOW_REQUEST("follow_request"); + FOLLOW_REQUEST("follow_request"), + CHAT_MESSAGE("pleroma:chat_mention"); companion object { @@ -52,7 +56,7 @@ data class Notification( } return UNKNOWN } - val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST) + val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST, CHAT_MESSAGE) } override fun toString(): String { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index de8592c2..62ad90c1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -778,6 +778,10 @@ public class NotificationsFragment extends SFragment implements List notificationsList = Notification.Type.Companion.getAsList(); List list = new ArrayList<>(); for (Notification.Type type : notificationsList) { + // ignore chat messages, as we don't work with them in main notification fragment + if(type == Notification.Type.CHAT_MESSAGE) + continue; + list.add(getNotificationText(type)); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt index 4eceeeca..9d2aba2b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt @@ -122,6 +122,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { true } } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_chat_messages) + key = PrefKeys.NOTIFICATION_FILTER_CHAT_MESSAGES + isIconSpaceReserved = false + isChecked = activeAccount.notificationsChatMessages + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsChatMessages = newValue as Boolean } + true + } + } } preferenceCategory(R.string.pref_title_notification_alerts) { category -> diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 907a2bb7..d7ec1ed9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -109,7 +109,8 @@ interface MastodonApi { fun notificationsWithAuth( @Header("Authorization") auth: String, @Header(DOMAIN_HEADER) domain: String, - @Query("with_muted") withMuted: Boolean? + @Query("with_muted") withMuted: Boolean?, + @Query("include_types[]") includeTypes: Set? ): Call> @POST("api/v1/notifications/clear") diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 154d1160..54969868 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Message import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -31,6 +32,7 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.service.SendTootService import com.keylesspalace.tusky.service.TootToSend import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.service.MessageToSend import com.keylesspalace.tusky.util.randomAlphanumericString import dagger.android.AndroidInjection import javax.inject.Inject @@ -55,38 +57,59 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT) val localAuthorId = intent.getStringExtra(NotificationHelper.KEY_CITED_AUTHOR_LOCAL) + val chatId = intent.getStringExtra(NotificationHelper.KEY_CHAT_ID) val account = accountManager.getAccountById(senderId) val notificationManager = NotificationManagerCompat.from(context) + if (account == null) { + Log.w(TAG, "Account \"$senderId\" not found in database. Aborting quick reply!") - if (intent.action == NotificationHelper.REPLY_ACTION) { + val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) + .setSmallIcon(R.drawable.ic_notify) + .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) + .setGroup(senderFullName) + .setDefaults(0) // So it doesn't ring twice, notify only in Target callback - val message = getReplyMessage(intent) + builder.setContentTitle(context.getString(R.string.error_generic)) + builder.setContentText(context.getString(R.string.error_sender_account_gone)) - if (account == null) { - Log.w(TAG, "Account \"$senderId\" not found in database. Aborting quick reply!") + builder.setSubText(senderFullName) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) + builder.setOnlyAlertOnce(true) - val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) - .setSmallIcon(R.drawable.ic_notify) - .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) - .setGroup(senderFullName) - .setDefaults(0) // So it doesn't ring twice, notify only in Target callback + notificationManager.notify(notificationId, builder.build()) + return + } - builder.setContentTitle(context.getString(R.string.error_generic)) - builder.setContentText(context.getString(R.string.error_sender_account_gone)) + if (intent.action == NotificationHelper.COMPOSE_ACTION) { + context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) - builder.setSubText(senderFullName) - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) - builder.setOnlyAlertOnce(true) + notificationManager.cancel(notificationId) - notificationManager.notify(notificationId, builder.build()) - } else { + accountManager.setActiveAccount(senderId) + + val composeIntent = ComposeActivity.startIntent(context, ComposeOptions( + inReplyToId = citedStatusId, + replyVisibility = visibility, + contentWarning = spoiler, + mentionedUsernames = mentions.toSet(), + replyingStatusAuthor = localAuthorId, + replyingStatusContent = citedText + )) + + composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + context.startActivity(composeIntent) + } else { + val message = getReplyMessage(intent) + + val sendIntent = if(intent.action == NotificationHelper.REPLY_ACTION) { val text = mentions.joinToString(" ", postfix = " ") { "@$it" } + message.toString() - val sendIntent = SendTootService.sendTootIntent( + SendTootService.sendTootIntent( context, TootToSend( text, @@ -110,45 +133,28 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { 0 ) ) - - context.startService(sendIntent) - - val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) - .setSmallIcon(R.drawable.ic_notify) - .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) - .setGroup(senderFullName) - .setDefaults(0) // So it doesn't ring twice, notify only in Target callback - - builder.setContentTitle(context.getString(R.string.status_sent)) - builder.setContentText(context.getString(R.string.status_sent_long)) - - builder.setSubText(senderFullName) - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) - builder.setOnlyAlertOnce(true) - - notificationManager.notify(notificationId, builder.build()) + } else { + SendTootService.sendMessageIntent(context, + MessageToSend(message.toString(), null, null, account.id, chatId!!, 0)) } - } else if (intent.action == NotificationHelper.COMPOSE_ACTION) { - context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) + context.startService(sendIntent) - notificationManager.cancel(notificationId) - - accountManager.setActiveAccount(senderId) + val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) + .setSmallIcon(R.drawable.ic_notify) + .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) + .setGroup(senderFullName) + .setDefaults(0) // So it doesn't ring twice, notify only in Target callback - val composeIntent = ComposeActivity.startIntent(context, ComposeOptions( - inReplyToId = citedStatusId, - replyVisibility = visibility, - contentWarning = spoiler, - mentionedUsernames = mentions.toSet(), - replyingStatusAuthor = localAuthorId, - replyingStatusContent = citedText - )) + builder.setContentTitle(context.getString(R.string.status_sent)) + builder.setContentText(context.getString(R.string.status_sent_long)) - composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + builder.setSubText(senderFullName) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) + builder.setOnlyAlertOnce(true) - context.startActivity(composeIntent) + notificationManager.notify(notificationId, builder.build()) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index efb2bb64..7c08f624 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -51,6 +51,7 @@ object PrefKeys { const val NOTIFICATION_ALERT_VIBRATE = "notificationAlertVibrate" const val NOTIFICATION_ALERT_SOUND = "notificationAlertSound" const val NOTIFICATION_FILTER_POLLS = "notificationFilterPolls" + const val NOTIFICATION_FILTER_CHAT_MESSAGES = "notificationFilterChatMessages" const val NOTIFICATION_FILTER_FAVS = "notificationFilterFavourites" const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs" const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt new file mode 100644 index 00000000..2ebbb61b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt @@ -0,0 +1,135 @@ +package com.keylesspalace.tusky.viewdata + +import android.text.Spanned +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Card +import com.keylesspalace.tusky.entity.Emoji +import java.util.* + + +abstract class ChatViewData { + abstract fun getViewDataId() : Int + abstract fun deepEquals(o: ChatViewData) : Boolean + + class Concrete(val account : Account, + val id: String, + val unread: Long, + val lastMessage: ChatMessageViewData.Concrete?, + val updatedAt: Date ) : ChatViewData() { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatViewData): Boolean { + if (o !is Concrete) return false + return Objects.equals(o.account, account) + && Objects.equals(o.id, id) + && o.unread == unread + && (lastMessage == o.lastMessage || (lastMessage != null && o.lastMessage != null && o.lastMessage.deepEquals(lastMessage))) + && Objects.equals(o.updatedAt, updatedAt) + } + + override fun hashCode(): Int { + return Objects.hash(account, id, unread, lastMessage, updatedAt) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Concrete) + } + } + + class Placeholder(val id: String, val isLoading: Boolean) : ChatViewData() { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatViewData): Boolean { + if( o !is Placeholder ) return false + return o.isLoading == isLoading && o.id == id + } + + override fun hashCode(): Int { + var result = if (isLoading) 1 else 0 + result = 31 * result + id.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Placeholder) + } + } +} + +abstract class ChatMessageViewData { + abstract fun getViewDataId() : Int + abstract fun deepEquals(o: ChatMessageViewData) : Boolean + + class Concrete(val id: String, + val content: Spanned?, + val chatId: String, + val accountId: String, + val createdAt: Date, + val attachment: Attachment?, + val emojis: List, + val card: Card?) : ChatMessageViewData() + { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatMessageViewData): Boolean { + if( o !is Concrete ) return false + + return Objects.equals(o.id, id) + && Objects.equals(o.content, content) + && Objects.equals(o.chatId, chatId) + && Objects.equals(o.accountId, accountId) + && Objects.equals(o.createdAt, createdAt) + && Objects.equals(o.attachment, attachment) + && Objects.equals(o.emojis, emojis) + && Objects.equals(o.card, card) + } + + override fun hashCode() : Int { + return Objects.hash(id, content, chatId, accountId, createdAt, attachment, card) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Concrete) + } + } + + class Placeholder(val id: String, val isLoading: Boolean) : ChatMessageViewData() { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatMessageViewData): Boolean { + if( o !is Placeholder) return false + return o.isLoading == isLoading && o.id == id + } + + override fun hashCode(): Int { + var result = if (isLoading) 1 else 0 + result = 31 * result + id.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Placeholder) + } + } +} \ No newline at end of file