chats: add new notification type pleroma:chat_mention (wip)

main
Alibek Omarov 4 years ago
parent d6e7853ddf
commit 828198dab7
  1. 12
      app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json
  2. 5
      app/src/husky/res/values-ru/strings.xml
  3. 6
      app/src/husky/res/values/strings.xml
  4. 74
      app/src/main/ic_bbcode.svg
  5. 74
      app/src/main/ic_html.svg
  6. 65
      app/src/main/ic_sticker.svg
  7. 50
      app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
  8. 4
      app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt
  9. 150
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
  10. 3
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt
  11. 1
      app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
  12. 3
      app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
  13. 1
      app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt
  14. 4
      app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
  15. 10
      app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt
  16. 4
      app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
  17. 11
      app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt
  18. 3
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
  19. 108
      app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
  20. 1
      app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
  21. 135
      app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt

@ -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')"
]
}
}

@ -19,7 +19,12 @@
<string name="notification_emoji_format">%s среагировал с %s на ваш пост</string>
<string name="notification_emoji_name">Эмодзи реакции</string>
<string name="notification_emoji_description">Уведомления о новых эмодзи реакциях</string>
<string name="notification_chat_message_format">%s отправил вам сообщение</string>
<string name="notification_chat_message_name">Сообщения</string>
<string name="notification_chat_message_description">Уведомления о новых сообщениях</string>
<string name="pref_title_default_formatting">Синтаксис форматирования по умолчанию(если поддерживается)</string>
<string name="pref_title_notification_filter_emoji">на мои посты отреагировали</string>
<string name="pref_title_notification_filter_chat_messages">получено новое сообщение</string>
<string name="pref_title_hide_muted_users">Скрывать заглушенных пользователей</string>
<string name="link">Ссылка</string>
</resources>

@ -26,9 +26,13 @@
<string name="notification_emoji_format">%s reacted with %s to your post</string>
<string name="notification_emoji_name">Emoji Reactions</string>
<string name="notification_emoji_description">Notifications about new emoji reactions</string>
<string name="notification_chat_message_format">%s sent you a message</string>
<string name="notification_chat_message_name">Chat Messages</string>
<string name="notification_chat_message_description">Notifications about new chat messages</string>
<string name="pref_title_default_formatting">Default formatting syntax(if supported by instance)</string>
<string name="pref_title_notification_filter_emoji">my posts are reacted with emojis</string>
<string name="pref_title_notification_filter_chat_messages">received a chat message</string>
<string name="pref_title_hide_muted_users">Hide muted users</string>
<string name="pref_title_enable_big_emojis">Enable bigger custom emojis</string>
<string name="pref_title_enable_experimental_stickers">Enable experimental Pleroma-FE stickers(if available)</string>
@ -37,6 +41,8 @@
<string name="attachment_type_video">Video</string>
<string name="attachment_type_audio">Audio</string>
<string name="attachment_type_unknown">Attachment</string>
<string name="link">Link</string> <!-- Web Link -->
<!-- REPLACEMENT FOR TUSKY STRINGS -->
<string name="action_toggle_visibility">Post visibility</string>

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24mm"
height="24mm"
viewBox="0 0 24 24"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="ic_bbcode.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="32.222537"
inkscape:cy="16.813518"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1876"
inkscape:window-height="1051"
inkscape:window-x="44"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-273)">
<g
aria-label="[ / ]"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.11111069px;line-height:578.50018311;font-family:Roboto;-inkscape-font-specification:Roboto;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
id="text821">
<path
d="m 5.4784346,278.1925 v 1.04731 H 4.1624082 v 11.5204 h 1.3160264 v 1.04731 H 2.8877229 V 278.1925 Z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path823" />
<path
d="M 14.532145,279.62566 10.3498,290.51905 H 9.2542592 l 4.1892358,-10.89339 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path825" />
<path
d="m 18.507785,279.23981 v -1.04731 h 2.604492 v 13.61502 h -2.604492 v -1.04731 h 1.322917 v -11.5204 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path827" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24mm"
height="24mm"
viewBox="0 0 24 24"
version="1.1"
id="svg8"
sodipodi:docname="ic_html.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="29.06581"
inkscape:cy="16.813518"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1876"
inkscape:window-height="1051"
inkscape:window-x="44"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-273)">
<g
aria-label="&lt;/&gt;"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.11111069px;line-height:578.50018311;font-family:Roboto;-inkscape-font-specification:Roboto;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
id="text821">
<path
d="m 7.9864639,288.23494 -5.636176,-2.61138 v -0.99908 l 5.636176,-2.60449 v 1.35048 l -4.299479,1.77078 4.299479,1.74321 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path888" />
<path
d="m 14.428792,279.5533 -4.182346,10.89339 h -1.09554 l 4.189236,-10.89339 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path890" />
<path
d="m 15.765489,282.00621 5.884223,2.60449 v 1.00597 l -5.884223,2.61138 v -1.31603 l 4.561306,-1.81212 -4.561306,-1.77766 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path892" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24mm"
height="24mm"
viewBox="0 0 24 24"
version="1.1"
id="svg8"
sodipodi:docname="ic_sticker.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="5.4548189"
inkscape:cy="71.290414"
inkscape:document-units="mm"
inkscape:current-layer="svg8"
showgrid="false"
inkscape:window-width="1876"
inkscape:window-height="1051"
inkscape:window-x="44"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:object-paths="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-global="true"
inkscape:snap-smooth-nodes="false"
inkscape:snap-midpoints="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
sodipodi:type="inkscape:offset"
inkscape:radius="0.61723572"
inkscape:original="M 17.623047 3.0683594 C 17.509421 3.0655494 17.393876 3.0680084 17.279297 3.078125 L 6.4902344 3.078125 C 4.5726672 3.0249708 2.9115899 4.8344018 3.0839844 6.7363281 C 3.0869044 10.385764 3.0795738 14.036202 3.0898438 17.685547 C 3.1383328 19.540878 4.9086202 21.084854 6.75 20.921875 L 11.855469 20.921875 L 12.105469 20.921875 L 12.355469 20.669922 L 20.644531 12.349609 L 20.927734 12.064453 L 20.927734 11.849609 C 20.927794 9.9997499 20.927719 8.1505896 20.923828 6.3007812 C 20.878728 4.5685513 19.327443 3.1105255 17.623047 3.0683594 z M 12.095703 4.0761719 C 13.93776 4.0746119 15.779621 4.0764408 17.621094 4.0917969 C 19.0166 4.1998379 20.08292 5.57683 19.927734 6.9511719 L 19.927734 11.849609 L 13.728516 11.849609 C 13.471455 11.849609 13.22575 11.902783 13.001953 11.998047 C 12.778155 12.093317 12.576116 12.230525 12.40625 12.400391 C 12.236384 12.570256 12.099172 12.772297 12.003906 12.996094 C 11.908636 13.219891 11.855469 13.465594 11.855469 13.722656 L 11.855469 19.921875 C 10.175931 19.921532 8.4977933 19.918927 6.8183594 19.908203 C 5.2022402 19.843685 3.9152358 18.271407 4.0839844 16.679688 C 4.0924326 13.222849 4.0677822 9.7667037 4.0976562 6.3105469 C 4.2045509 5.0752806 5.3270093 4.0566644 6.5683594 4.078125 C 8.4101806 4.082465 10.253646 4.0777473 12.095703 4.0761719 z M 14.162109 12.849609 L 15.355469 12.849609 L 18.732422 12.849609 L 12.855469 18.75 L 12.855469 15.349609 L 12.855469 14.15625 C 12.855469 13.977032 12.892568 13.804462 12.958984 13.648438 C 13.025404 13.492411 13.121807 13.352801 13.240234 13.234375 C 13.35866 13.115948 13.498271 13.019543 13.654297 12.953125 C 13.810323 12.886705 13.982892 12.849609 14.162109 12.849609 z "
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:40;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path4812"
d="m 17.638672,2.4511719 c -0.11531,-0.00285 -0.236952,0.00108 -0.359375,0.00977 H 6.5078125 C 4.1925376,2.396759 2.2610006,4.5009931 2.46875,6.7929688 l -0.00195,-0.056641 c 0.00292,3.6482799 -0.00442,7.2998419 0.00586,10.9511719 a 0.61729744,0.61729744 0 0 0 0,0.01367 c 0.058506,2.238617 2.1150294,4.032162 4.3320313,3.835937 L 6.75,21.539062 h 5.105469 0.25 a 0.61729744,0.61729744 0 0 0 0.4375,-0.18164 l 0.25,-0.251953 8.289062,-8.320313 L 21.365234,12.5 a 0.61729744,0.61729744 0 0 0 0.179688,-0.435547 v -0.214844 c 6e-5,-1.8498887 -1.4e-5,-3.7001239 -0.0039,-5.5507809 a 0.61729744,0.61729744 0 0 0 0,-0.013672 C 21.486566,4.1938021 19.685497,2.5018096 17.638672,2.4511719 Z m -5.542969,2.2421875 c 1.829572,-0.00155 3.657616,5.622e-4 5.484375,0.015625 1.014403,0.082688 1.848797,1.1605015 1.734375,2.1738281 a 0.61729744,0.61729744 0 0 0 -0.0039,0.068359 v 4.2812501 h -5.582031 c -0.34218,0 -0.671313,0.07066 -0.96875,0.197266 -0.297129,0.126486 -0.563672,0.309764 -0.789063,0.535156 -0.225391,0.225389 -0.40867,0.491924 -0.535156,0.789062 -0.126626,0.297454 -0.197266,0.626581 -0.197266,0.96875 v 5.580078 C 9.7727653,19.301874 8.3083381,19.300307 6.84375,19.291016 5.6139527,19.24192 4.5676965,17.9663 4.6972656,16.744141 a 0.61729744,0.61729744 0 0 0 0.00391,-0.0625 c 0.00842,-3.446433 -0.015574,-6.8868576 0.013672,-10.322266 0.079886,-0.892733 0.9478119,-1.6795516 1.84375,-1.6640625 a 0.61729744,0.61729744 0 0 0 0.00781,0 c 1.8438457,0.00434 3.6879418,-3.783e-4 5.5292968,-0.00195 z m 2.066406,8.7734376 h 1.19336 1.890625 l -3.773438,3.789062 v -1.90625 -1.193359 c 0,-0.09404 0.02007,-0.184299 0.05469,-0.265625 0.03409,-0.08008 0.08309,-0.155357 0.148437,-0.220703 0.06535,-0.06535 0.140636,-0.114354 0.220703,-0.148438 0.08132,-0.03462 0.171582,-0.05469 0.265625,-0.05469 z"
transform="translate(8.2803066e-4,-0.00103277)" />
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -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<List<Notification>> {
override fun onFailure(call: Call<List<Notification>>, t: Throwable) {
}
override fun onResponse(call: Call<List<Notification>>, response: Response<List<Notification>>) {
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)
}

@ -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) {

@ -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<String> 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<String> 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<NotificationChannel> 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;
}

@ -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) {

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

@ -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");
}
};
}

@ -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',

@ -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
})
}
}

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

@ -778,6 +778,10 @@ public class NotificationsFragment extends SFragment implements
List<Notification.Type> notificationsList = Notification.Type.Companion.getAsList();
List<String> 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));
}

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

@ -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<String>?
): Call<List<Notification>>
@POST("api/v1/notifications/clear")

@ -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())
}
}

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

@ -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<Emoji>,
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)
}
}
}
Loading…
Cancel
Save