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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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