Merge branch '794-global-notifications' of https://github.com/cvuorinen/habitica-android into cvuorinen-794-global-notifications
# Conflicts: # Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt # Habitica/src/main/java/com/habitrpg/android/habitica/modules/ApiModule.java # Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt
|
|
@ -64,6 +64,13 @@
|
|||
android:pathPattern="/settings/.*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.activities.NotificationsActivity"
|
||||
android:parentActivityName=".ui.activities.MainActivity"
|
||||
android:label="@string/notifications"
|
||||
android:screenOrientation="portrait"
|
||||
tools:ignore="UnusedAttribute">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.activities.FixCharacterValuesActivity"
|
||||
android:parentActivityName=".ui.activities.PrefsActivity"
|
||||
|
|
|
|||
BIN
Habitica/res/drawable-hdpi/no_notifications.webp
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
Habitica/res/drawable-hdpi/notification_close.webp
Normal file
|
After Width: | Height: | Size: 154 B |
BIN
Habitica/res/drawable-hdpi/notification_mystery_item.webp
Normal file
|
After Width: | Height: | Size: 956 B |
BIN
Habitica/res/drawable-hdpi/notification_stat_sparkles.webp
Normal file
|
After Width: | Height: | Size: 712 B |
BIN
Habitica/res/drawable-hdpi/notifications_bailey.webp
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
Habitica/res/drawable-mdpi/menu_notifications.webp
Normal file
|
After Width: | Height: | Size: 164 B |
BIN
Habitica/res/drawable-mdpi/no_notifications.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Habitica/res/drawable-mdpi/notification_close.webp
Normal file
|
After Width: | Height: | Size: 138 B |
BIN
Habitica/res/drawable-mdpi/notification_mystery_item.webp
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
Habitica/res/drawable-mdpi/notification_stat_sparkles.webp
Normal file
|
After Width: | Height: | Size: 522 B |
BIN
Habitica/res/drawable-mdpi/notifications_bailey.webp
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
Habitica/res/drawable-xhdpi/menu_notifications.webp
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
Habitica/res/drawable-xhdpi/no_notifications.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
Habitica/res/drawable-xhdpi/notification_close.webp
Normal file
|
After Width: | Height: | Size: 190 B |
BIN
Habitica/res/drawable-xhdpi/notification_mystery_item.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Habitica/res/drawable-xhdpi/notification_stat_sparkles.webp
Normal file
|
After Width: | Height: | Size: 960 B |
BIN
Habitica/res/drawable-xhdpi/notifications_bailey.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
Habitica/res/drawable-xxhdpi/menu_notifications.webp
Normal file
|
After Width: | Height: | Size: 292 B |
BIN
Habitica/res/drawable-xxhdpi/no_notifications.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
Habitica/res/drawable-xxhdpi/notification_close.webp
Normal file
|
After Width: | Height: | Size: 224 B |
BIN
Habitica/res/drawable-xxhdpi/notification_mystery_item.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Habitica/res/drawable-xxhdpi/notification_stat_sparkles.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Habitica/res/drawable-xxhdpi/notifications_bailey.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
|
@ -4,10 +4,5 @@
|
|||
<corners
|
||||
android:radius="10dip"/>
|
||||
<solid
|
||||
android:color="@color/red_50" />
|
||||
<padding
|
||||
android:left="5dip"
|
||||
android:right="5dip"
|
||||
android:top="5dip"
|
||||
android:bottom="5dip" />
|
||||
android:color="@color/brand_400" />
|
||||
</shape>
|
||||
11
Habitica/res/drawable/badge_gray.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:radius="100dip"/>
|
||||
<solid
|
||||
android:color="@color/gray_600" />
|
||||
<padding
|
||||
android:left="6dip"
|
||||
android:right="6dip" />
|
||||
</shape>
|
||||
54
Habitica/res/layout/activity_notifications.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.activities.NotificationsActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="@style/Toolbar"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Dark" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="top|center"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/notifications_refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/notification_items"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="?android:listDivider"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle" />
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="1dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
|
@ -58,10 +58,42 @@
|
|||
</LinearLayout>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/messagesButtonWrapper"
|
||||
android:layout_width="45dp"
|
||||
android:id="@+id/notificationsButtonWrapper"
|
||||
android:layout_width="33dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="16dp">
|
||||
<ImageView
|
||||
android:id="@+id/notificationsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/menu_notifications"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notificationsBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:layout_alignLeft="@id/notificationsButton"
|
||||
android:layout_alignTop="@id/notificationsButton"
|
||||
android:layout_marginLeft="13dp"
|
||||
android:layout_marginTop="-13dp"
|
||||
android:background="@drawable/badge_circle"
|
||||
android:gravity="center"
|
||||
android:minWidth="20dp"
|
||||
android:paddingTop="0dp"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="1"
|
||||
tools:visibility="visible" />
|
||||
</RelativeLayout>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/messagesButtonWrapper"
|
||||
android:layout_width="33dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="8dp">
|
||||
<ImageView
|
||||
android:id="@+id/messagesButton"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -70,21 +102,24 @@
|
|||
android:src="@drawable/menu_messages"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messagesBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:paddingTop="0dp"
|
||||
android:layout_alignTop="@id/messagesButton"
|
||||
android:layout_alignLeft="@id/messagesButton"
|
||||
tools:text="1"
|
||||
android:layout_alignTop="@id/messagesButton"
|
||||
android:layout_marginLeft="13dp"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:background="@drawable/badge_circle"
|
||||
android:gravity="center"
|
||||
android:minWidth="20dp"
|
||||
android:paddingTop="0dp"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="12sp"
|
||||
android:background="@drawable/badge_circle"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:layout_marginLeft="13dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
tools:text="1"
|
||||
tools:visibility="visible" />
|
||||
</RelativeLayout>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/settingsButtonWrapper"
|
||||
|
|
@ -98,21 +133,25 @@
|
|||
android:background="@color/transparent"
|
||||
android:src="@drawable/menu_settings"
|
||||
android:layout_centerVertical="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/settingsBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:paddingTop="0dp"
|
||||
android:layout_alignTop="@id/settingsButton"
|
||||
android:layout_alignLeft="@id/settingsButton"
|
||||
tools:text="1"
|
||||
android:layout_alignTop="@id/settingsButton"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="13dp"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:background="@drawable/badge_circle"
|
||||
android:gravity="center"
|
||||
android:minWidth="20dp"
|
||||
android:paddingTop="0dp"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="12sp"
|
||||
android:background="@drawable/badge_circle"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:layout_marginLeft="13dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
tools:text="1"
|
||||
tools:visibility="visible" />
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
|||
52
Habitica/res/layout/no_notifications.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/spacing_large">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/spacing_medium"
|
||||
android:paddingTop="44dp"
|
||||
android:paddingRight="@dimen/spacing_medium">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/noNotifications"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:src="@drawable/no_notifications" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="40dp"
|
||||
android:paddingTop="26dp"
|
||||
android:paddingRight="40dp">
|
||||
|
||||
<TextView
|
||||
style="@style/SectionTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_notifications_title" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="5dp"
|
||||
android:paddingTop="16dp"
|
||||
android:text="@string/no_notifications_text" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
42
Habitica/res/layout/notification_item.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/notification_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingRight="20dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="20dp"
|
||||
android:background="@color/transparent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:layout_weight="1"
|
||||
android:lineSpacingExtra="5dp"
|
||||
android:text="Message" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/dismiss_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:background="@color/transparent"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/notification_close" />
|
||||
|
||||
</LinearLayout>
|
||||
53
Habitica/res/layout/notifications_header.xml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingRight="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="@dimen/spacing_medium">
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/title"
|
||||
style="@style/Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dip"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/notifications"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/gray_300"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notifications_title_badge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:background="@drawable/badge_gray"
|
||||
android:gravity="center"
|
||||
android:minWidth="24dp"
|
||||
android:textColor="@color/gray_300"
|
||||
android:textSize="12sp"
|
||||
tools:text="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/dismiss_all_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/dismiss_all"
|
||||
android:textColor="@color/brand_400" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -210,6 +210,11 @@
|
|||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
</fragment>
|
||||
<activity
|
||||
android:id="@+id/notificationsActivity"
|
||||
android:name="com.habitrpg.android.habitica.ui.activities.NotificationsActivity"
|
||||
android:label="@string/notifications">
|
||||
</activity>
|
||||
<fragment
|
||||
android:id="@+id/petDetailRecyclerFragment"
|
||||
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.stable.PetDetailRecyclerFragment"
|
||||
|
|
|
|||
|
|
@ -534,6 +534,7 @@
|
|||
<string name="open_settings">Open Settings</string>
|
||||
<string name="dont_keep_activities_warning">It seems like you have the Developer option \“Don\'t keep Activities\” active. Currently this option causes issues with the habitica app, so we suggest disabling it.</string>
|
||||
<string name="inbox">Messages</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
<string name="FAQ">Frequently Asked Questions</string>
|
||||
<string name="special">Special</string>
|
||||
<string name="gem_for_gold_description">Because you subscribe to Habitica, you can purchase a number of Gems each month using Gold.</string>
|
||||
|
|
@ -858,6 +859,14 @@
|
|||
<string name="discover">Discover</string>
|
||||
<string name="damage_paused">Damage paused</string>
|
||||
<string name="preference_push_important_announcements">Important Announcements</string>
|
||||
<string name="no_notifications_title">You’re all caught up!</string>
|
||||
<string name="no_notifications_text">The notification fairies give you a raucous round of applause! Well done!</string>
|
||||
<string name="dismiss_all">Dismiss All</string>
|
||||
<string name="new_bailey_update">New Bailey Update!</string>
|
||||
<string name="new_msg_guild"><![CDATA[<b>%1$s</b> has new posts]]></string>
|
||||
<string name="new_msg_party"><![CDATA[Your Party, <b>%1$s</b>, has new posts]]></string>
|
||||
<string name="unallocated_stats_points"><![CDATA[You have <b>%1$s unallocated Stat Points</b>]]></string>
|
||||
<string name="new_subscriber_item"><![CDATA[You have new <b>Mystery Items</b>]]></string>
|
||||
<string name="create">Create</string>
|
||||
<string name="only_leader_create_challenge">Only leader can create Challenges</string>
|
||||
<string name="create_party">Create Party</string>
|
||||
|
|
|
|||
|
|
@ -348,6 +348,12 @@ public interface ApiService {
|
|||
@POST("notifications/{notificationId}/read")
|
||||
Flowable<HabitResponse<List>> readNotification(@Path("notificationId") String notificationId);
|
||||
|
||||
@POST("notifications/read")
|
||||
Flowable<HabitResponse<List>> readNotifications(@Body Map<String, List<String>> notificationIds);
|
||||
|
||||
@POST("notifications/see")
|
||||
Flowable<HabitResponse<List>> seeNotifications(@Body Map<String, List<String>> notificationIds);
|
||||
|
||||
@POST("user/open-mystery-item")
|
||||
Flowable<HabitResponse<Equipment>> openMysteryItem();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.google.gson.reflect.TypeToken;
|
|||
import com.habitrpg.android.habitica.models.Achievement;
|
||||
import com.habitrpg.android.habitica.models.ContentResult;
|
||||
import com.habitrpg.android.habitica.models.FAQArticle;
|
||||
import com.habitrpg.android.habitica.models.Notification;
|
||||
import com.habitrpg.android.habitica.models.Skill;
|
||||
import com.habitrpg.android.habitica.models.Tag;
|
||||
import com.habitrpg.android.habitica.models.TutorialStep;
|
||||
|
|
@ -45,6 +46,7 @@ import com.habitrpg.android.habitica.utils.GroupSerialization;
|
|||
import com.habitrpg.android.habitica.utils.MemberSerialization;
|
||||
import com.habitrpg.android.habitica.utils.OwnedItemListDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.OwnedMountListDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.NotificationDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.OwnedPetListDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.PurchasedDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.QuestCollectDeserializer;
|
||||
|
|
@ -118,6 +120,7 @@ public class GSonFactoryCreator {
|
|||
.registerTypeAdapter(Member.class, new MemberSerialization())
|
||||
.registerTypeAdapter(WorldState.class, new WorldStateSerialization())
|
||||
.registerTypeAdapter(FindUsernameResult.class, new FindUsernameResultDeserializer())
|
||||
.registerTypeAdapter(Notification.class, new NotificationDeserializer())
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
.create();
|
||||
return GsonConverterFactory.create(gson);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.habitrpg.android.habitica.ui.activities.IntroActivity;
|
|||
import com.habitrpg.android.habitica.ui.activities.LoginActivity;
|
||||
import com.habitrpg.android.habitica.ui.activities.MainActivity;
|
||||
import com.habitrpg.android.habitica.ui.activities.MaintenanceActivity;
|
||||
import com.habitrpg.android.habitica.ui.activities.NotificationsActivity;
|
||||
import com.habitrpg.android.habitica.ui.activities.GroupInviteActivity;
|
||||
import com.habitrpg.android.habitica.ui.activities.PrefsActivity;
|
||||
import com.habitrpg.android.habitica.ui.activities.ReportMessageActivity;
|
||||
|
|
@ -94,6 +95,7 @@ import com.habitrpg.android.habitica.ui.fragments.social.party.PartyMemberListFr
|
|||
import com.habitrpg.android.habitica.ui.fragments.tasks.TaskRecyclerViewFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment;
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel;
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel;
|
||||
import com.habitrpg.android.habitica.ui.views.social.ChatBarView;
|
||||
import com.habitrpg.android.habitica.ui.views.stats.BulkAllocateStatsDialog;
|
||||
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog;
|
||||
|
|
@ -134,6 +136,8 @@ public interface AppComponent {
|
|||
|
||||
void inject(PrefsActivity prefsActivity);
|
||||
|
||||
void inject(NotificationsActivity notificationsActivity);
|
||||
|
||||
void inject(SetupActivity setupActivity);
|
||||
|
||||
void inject(SkillTasksActivity skillTasksActivity);
|
||||
|
|
@ -302,6 +306,8 @@ public interface AppComponent {
|
|||
|
||||
void inject(@NotNull GroupViewModel viewModel);
|
||||
|
||||
void inject(@NotNull NotificationsViewModel viewModel);
|
||||
|
||||
void inject(@NotNull ChatFragment chatFragment);
|
||||
|
||||
void inject(@NotNull GiftIAPActivity giftIAPActivity);
|
||||
|
|
|
|||
|
|
@ -212,6 +212,8 @@ interface ApiClient {
|
|||
|
||||
// Notifications
|
||||
fun readNotification(notificationId: String): Flowable<List<*>>
|
||||
fun readNotifications(notificationIds: Map<String, List<String>>): Flowable<List<*>>
|
||||
fun seeNotifications(notificationIds: Map<String, List<String>>): Flowable<List<*>>
|
||||
|
||||
fun getErrorResponse(throwable: HttpException): ErrorResponse
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ interface UserRepository : BaseRepository {
|
|||
fun runCron()
|
||||
|
||||
fun readNotification(id: String): Flowable<List<*>>
|
||||
fun readNotifications(notificationIds: Map<String, List<String>>): Flowable<List<*>>
|
||||
fun seeNotifications(notificationIds: Map<String, List<String>>): Flowable<List<*>>
|
||||
|
||||
fun changeCustomDayStart(dayStartTime: Int): Flowable<User>
|
||||
|
||||
|
|
|
|||
|
|
@ -172,6 +172,12 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
|
|||
|
||||
override fun readNotification(id: String): Flowable<List<*>> = apiClient.readNotification(id)
|
||||
|
||||
override fun readNotifications(notificationIds: Map<String, List<String>>): Flowable<List<*>> =
|
||||
apiClient.readNotifications(notificationIds)
|
||||
|
||||
override fun seeNotifications(notificationIds: Map<String, List<String>>): Flowable<List<*>> =
|
||||
apiClient.seeNotifications(notificationIds)
|
||||
|
||||
override fun changeCustomDayStart(dayStartTime: Int): Flowable<User> {
|
||||
val updateObject = HashMap<String, Any>()
|
||||
updateObject["dayStart"] = dayStartTime
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package com.habitrpg.android.habitica.helpers
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.data.ApiClient
|
||||
import com.habitrpg.android.habitica.events.ShowCheckinDialog
|
||||
import com.habitrpg.android.habitica.events.ShowSnackbarEvent
|
||||
import com.habitrpg.android.habitica.models.Notification
|
||||
import com.habitrpg.android.habitica.models.notifications.LoginIncentiveData
|
||||
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
|
||||
import io.reactivex.functions.Consumer
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
import java.util.HashMap
|
||||
|
||||
/**
|
||||
* Created by krh12 on 12/9/2016.
|
||||
*/
|
||||
|
||||
class NotificationsManager (private val context: Context) {
|
||||
// @TODO: A queue for displaying alert dialogues
|
||||
|
||||
private val seenNotifications: MutableMap<String, Boolean>
|
||||
private var apiClient: ApiClient? = null
|
||||
|
||||
private val notifications: BehaviorSubject<List<Notification>>
|
||||
|
||||
init {
|
||||
this.seenNotifications = HashMap()
|
||||
this.notifications = BehaviorSubject.create()
|
||||
}
|
||||
|
||||
fun setNotifications(current: List<Notification>) {
|
||||
this.notifications.onNext(current)
|
||||
|
||||
this.handlePopupNotifications(current)
|
||||
}
|
||||
|
||||
fun getNotifications(): Flowable<List<Notification>> {
|
||||
return this.notifications.toFlowable(BackpressureStrategy.LATEST)
|
||||
}
|
||||
|
||||
fun getNotification(id: String): Notification? {
|
||||
return this.notifications.value?.find { it.id == id }
|
||||
}
|
||||
|
||||
fun setApiClient(apiClient: ApiClient?) {
|
||||
this.apiClient = apiClient
|
||||
}
|
||||
|
||||
fun handlePopupNotifications(notifications: List<Notification>): Boolean? {
|
||||
notifications
|
||||
.filter { !this.seenNotifications.containsKey(it.id) }
|
||||
.map {
|
||||
val notificationDisplayed = when (it.type) {
|
||||
Notification.Type.LOGIN_INCENTIVE.type -> displayLoginIncentiveNotification(it)
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (notificationDisplayed == true) {
|
||||
this.seenNotifications[it.id] = true
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun displayLoginIncentiveNotification(notification: Notification): Boolean? {
|
||||
val notificationData = notification.data as LoginIncentiveData?
|
||||
val nextUnlockText = context.getString(R.string.nextPrizeUnlocks, notificationData!!.nextRewardAt)
|
||||
if (notificationData.rewardKey != null) {
|
||||
val event = ShowCheckinDialog()
|
||||
event.notification = notification
|
||||
event.nextUnlockText = nextUnlockText
|
||||
EventBus.getDefault().post(event)
|
||||
} else {
|
||||
val event = ShowSnackbarEvent()
|
||||
event.title = notificationData.message
|
||||
event.text = nextUnlockText
|
||||
event.type = HabiticaSnackbar.SnackbarDisplayType.BLUE
|
||||
EventBus.getDefault().post(event)
|
||||
if (apiClient != null) {
|
||||
// @TODO: This should be handled somewhere else? MAybe we notifiy via event
|
||||
apiClient!!.readNotification(notification.id)
|
||||
.subscribe(Consumer {}, RxErrorHandler.handleEmptyError())
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
package com.habitrpg.android.habitica.models;
|
||||
|
||||
/**
|
||||
* Created by krh12 on 11/28/2016.
|
||||
*/
|
||||
|
||||
import com.habitrpg.android.habitica.models.notifications.NotificationData;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Notification {
|
||||
|
||||
public NotificationData data;
|
||||
private String type;
|
||||
private String createdAt;
|
||||
private String id;
|
||||
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
|
||||
|
||||
/**
|
||||
* @return The type
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type The type
|
||||
*/
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The createdAt
|
||||
*/
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param createdAt The createdAt
|
||||
*/
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
* The data
|
||||
*/
|
||||
// public T getData() {
|
||||
// return data;
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* The data
|
||||
*/
|
||||
// public void setData(T data) {
|
||||
// this.data = data;
|
||||
// }
|
||||
|
||||
/**
|
||||
* @return The id
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The id
|
||||
*/
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAdditionalProperties() {
|
||||
return this.additionalProperties;
|
||||
}
|
||||
|
||||
public void setAdditionalProperty(String name, Object value) {
|
||||
this.additionalProperties.put(name, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.habitrpg.android.habitica.models
|
||||
|
||||
import com.habitrpg.android.habitica.models.notifications.*
|
||||
|
||||
class Notification {
|
||||
enum class Type(val type: String) {
|
||||
LOGIN_INCENTIVE("LOGIN_INCENTIVE"),
|
||||
NEW_STUFF("NEW_STUFF"),
|
||||
NEW_CHAT_MESSAGE("NEW_CHAT_MESSAGE"),
|
||||
NEW_MYSTERY_ITEMS("NEW_MYSTERY_ITEMS"),
|
||||
GROUP_TASK_NEEDS_WORK("GROUP_TASK_NEEDS_WORK"),
|
||||
GROUP_TASK_APPROVED("GROUP_TASK_APPROVED"),
|
||||
UNALLOCATED_STATS_POINTS("UNALLOCATED_STATS_POINTS");
|
||||
}
|
||||
|
||||
var id: String = ""
|
||||
|
||||
var type: String? = null
|
||||
var seen: Boolean? = null
|
||||
|
||||
var data: NotificationData? = null
|
||||
|
||||
fun getDataType(): java.lang.reflect.Type? {
|
||||
return when (type) {
|
||||
Type.LOGIN_INCENTIVE.type -> LoginIncentiveData::class.java
|
||||
Type.NEW_STUFF.type -> NewStuffData::class.java
|
||||
Type.NEW_CHAT_MESSAGE.type -> NewChatMessageData::class.java
|
||||
Type.GROUP_TASK_NEEDS_WORK.type -> GroupTaskNeedsWorkData::class.java
|
||||
Type.GROUP_TASK_APPROVED.type -> GroupTaskApprovedData::class.java
|
||||
Type.UNALLOCATED_STATS_POINTS.type -> UnallocatedPointsData::class.java
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
open class GroupTaskApprovedData : NotificationData {
|
||||
var groupId: String = ""
|
||||
|
||||
var message: String? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
open class GroupTaskNeedsWorkData : NotificationData {
|
||||
var group: NotificationGroup? = null
|
||||
|
||||
var message: String? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
open class LoginIncentiveData : NotificationData {
|
||||
|
||||
var message: String? = null
|
||||
var nextRewardAt: Int? = null
|
||||
var rewardText: String? = null
|
||||
var rewardKey: List<String>? = null
|
||||
var reward: List<Reward>? = null
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
open class NewChatMessageData : NotificationData {
|
||||
var group: NotificationGroup? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
open class NewStuffData : NotificationData {
|
||||
var title: String? = null
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.habitrpg.android.habitica.models.notifications;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by krh12 on 11/30/2016.
|
||||
*/
|
||||
|
||||
public class NotificationData {
|
||||
|
||||
public String groupId;
|
||||
public String message;
|
||||
public Integer nextRewardAt;
|
||||
public String rewardText;
|
||||
public List<String> rewardKey;
|
||||
public List<Reward> reward;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
interface NotificationData
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
open class NotificationGroup {
|
||||
var id: String = ""
|
||||
|
||||
var name: String? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.habitrpg.android.habitica.models.notifications
|
||||
|
||||
open class UnallocatedPointsData : NotificationData {
|
||||
var points: Int? = null
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import com.habitrpg.android.habitica.api.HostConfig;
|
|||
import com.habitrpg.android.habitica.api.MaintenanceApiService;
|
||||
import com.habitrpg.android.habitica.data.ApiClient;
|
||||
import com.habitrpg.android.habitica.data.implementation.ApiClientImpl;
|
||||
import com.habitrpg.android.habitica.helpers.NotificationsManager;
|
||||
import com.habitrpg.android.habitica.helpers.KeyHelper;
|
||||
import com.habitrpg.android.habitica.helpers.PopupNotificationsManager;
|
||||
import com.habitrpg.android.habitica.proxy.CrashlyticsProxy;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
|
@ -36,14 +36,14 @@ public class ApiModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
public PopupNotificationsManager providesPopupNotificationsManager(Context context) {
|
||||
return new PopupNotificationsManager(context);
|
||||
public NotificationsManager providesPopupNotificationsManager(Context context) {
|
||||
return new NotificationsManager(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public ApiClient providesApiHelper(GsonConverterFactory gsonConverter, HostConfig hostConfig, CrashlyticsProxy crashlyticsProxy, PopupNotificationsManager popupNotificationsManager, Context context) {
|
||||
return new ApiClientImpl(gsonConverter, hostConfig, crashlyticsProxy, popupNotificationsManager, context);
|
||||
public ApiClient providesApiHelper(GsonConverterFactory gsonConverter, HostConfig hostConfig, CrashlyticsProxy crashlyticsProxy, NotificationsManager notificationsManager, Context context) {
|
||||
return new ApiClientImpl(gsonConverter, hostConfig, crashlyticsProxy, notificationsManager, context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
package com.habitrpg.android.habitica.ui.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.components.AppComponent
|
||||
import com.habitrpg.android.habitica.helpers.RxErrorHandler
|
||||
import com.habitrpg.android.habitica.models.Notification
|
||||
import com.habitrpg.android.habitica.models.notifications.*
|
||||
import com.habitrpg.android.habitica.ui.activities.MainActivity.Companion.NOTIFICATION_CLICK
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel
|
||||
import io.reactivex.functions.Consumer
|
||||
import kotlinx.android.synthetic.main.activity_notifications.*
|
||||
|
||||
class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
lateinit var viewModel: NotificationsViewModel
|
||||
|
||||
lateinit var inflater: LayoutInflater
|
||||
|
||||
override fun getLayoutResId(): Int = R.layout.activity_notifications
|
||||
|
||||
private var notifications: List<Notification> = emptyList()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setupToolbar(toolbar)
|
||||
|
||||
inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
viewModel = ViewModelProviders.of(this)
|
||||
.get(NotificationsViewModel::class.java)
|
||||
|
||||
compositeSubscription.add(viewModel.getNotifications().subscribe(Consumer {
|
||||
this.setNotifications(it)
|
||||
viewModel.markNotificationsAsSeen(it)
|
||||
}, RxErrorHandler.handleEmptyError()))
|
||||
|
||||
notifications_refresh_layout?.setOnRefreshListener(this)
|
||||
}
|
||||
|
||||
override fun injectActivity(component: AppComponent?) {
|
||||
component?.inject(this)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onSupportNavigateUp()
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
notifications_refresh_layout.isRefreshing = true
|
||||
|
||||
compositeSubscription.add(viewModel.refreshNotifications().subscribe(Consumer {
|
||||
notifications_refresh_layout.isRefreshing = false
|
||||
}, RxErrorHandler.handleEmptyError()))
|
||||
}
|
||||
|
||||
private fun setNotifications(notifications: List<Notification>) {
|
||||
this.notifications = notifications
|
||||
|
||||
if (notification_items == null) {
|
||||
return
|
||||
}
|
||||
|
||||
notification_items.removeAllViewsInLayout()
|
||||
|
||||
when {
|
||||
notifications.isEmpty() -> displayNoNotificationsView()
|
||||
else -> displayNotificationsListView(notifications)
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayNoNotificationsView() {
|
||||
notification_items.showDividers = LinearLayout.SHOW_DIVIDER_NONE
|
||||
|
||||
notification_items.addView(
|
||||
inflater.inflate(R.layout.no_notifications, notification_items, false)
|
||||
)
|
||||
}
|
||||
|
||||
private fun displayNotificationsListView(notifications: List<Notification>) {
|
||||
notification_items.showDividers = LinearLayout.SHOW_DIVIDER_MIDDLE or LinearLayout.SHOW_DIVIDER_END
|
||||
|
||||
notification_items.addView(
|
||||
createNotificationsHeaderView(notifications.count())
|
||||
)
|
||||
|
||||
notifications.map {
|
||||
val item: View? = when (it.type) {
|
||||
Notification.Type.NEW_CHAT_MESSAGE.type -> createNewChatMessageNotification(it)
|
||||
Notification.Type.NEW_STUFF.type -> createNewStuffNotification(it)
|
||||
Notification.Type.UNALLOCATED_STATS_POINTS.type -> createUnallocatedStatsNotification(it)
|
||||
Notification.Type.NEW_MYSTERY_ITEMS.type -> createMysteryItemsNotification(it)
|
||||
Notification.Type.GROUP_TASK_NEEDS_WORK.type -> createGroupTaskNeedsWorkNotification(it)
|
||||
Notification.Type.GROUP_TASK_APPROVED.type -> createGroupTaskApprovedNotification(it)
|
||||
//TODO rest of the notification types
|
||||
else -> null
|
||||
}
|
||||
|
||||
notification_items.addView(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationsHeaderView(notificationCount: Int): View? {
|
||||
val header = inflater.inflate(R.layout.notifications_header, notification_items, false)
|
||||
|
||||
val badge = header?.findViewById(R.id.notifications_title_badge) as? TextView
|
||||
badge?.text = notificationCount.toString()
|
||||
|
||||
val dismissAllButton = header?.findViewById(R.id.dismiss_all_button) as? Button
|
||||
dismissAllButton?.setOnClickListener { viewModel.dismissAllNotifications(notifications) }
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
private fun createNewChatMessageNotification(notification: Notification): View? {
|
||||
val data = notification.data as? NewChatMessageData
|
||||
val stringId = if (viewModel.isPartyMessage(data)) R.string.new_msg_party else R.string.new_msg_guild
|
||||
|
||||
return createNotificationItem(
|
||||
notification,
|
||||
fromHtml(getString(stringId, data?.group?.name))
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNewStuffNotification(notification: Notification): View? {
|
||||
val data = notification.data as? NewStuffData
|
||||
val text = fromHtml("<b>" + getString(R.string.new_bailey_update) + "</b><br>" + data?.title)
|
||||
|
||||
return createNotificationItem(
|
||||
notification,
|
||||
text,
|
||||
R.drawable.notifications_bailey
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUnallocatedStatsNotification(notification: Notification): View? {
|
||||
val data = notification.data as? UnallocatedPointsData
|
||||
|
||||
return createNotificationItem(
|
||||
notification,
|
||||
fromHtml(getString(R.string.unallocated_stats_points, data?.points.toString())),
|
||||
R.drawable.notification_stat_sparkles
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMysteryItemsNotification(notification: Notification): View? {
|
||||
return createNotificationItem(
|
||||
notification,
|
||||
fromHtml(getString(R.string.new_subscriber_item)),
|
||||
R.drawable.notification_mystery_item
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGroupTaskNeedsWorkNotification(notification: Notification): View? {
|
||||
val data = notification.data as? GroupTaskNeedsWorkData
|
||||
val message = convertGroupMessageHtml(data?.message ?: "")
|
||||
|
||||
return createNotificationItem(
|
||||
notification,
|
||||
fromHtml(message),
|
||||
null,
|
||||
R.color.yellow_5
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGroupTaskApprovedNotification(notification: Notification): View? {
|
||||
val data = notification.data as? GroupTaskApprovedData
|
||||
val message = convertGroupMessageHtml(data?.message ?: "")
|
||||
|
||||
return createNotificationItem(
|
||||
notification,
|
||||
fromHtml(message),
|
||||
null,
|
||||
R.color.green_10
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Group task notifications have the message text in the notification data as HTML
|
||||
* with <span class="notification-bold"> tags around emphasized words. So we just
|
||||
* convert the span-tags to strong-tags to display correct parts as bold, since
|
||||
* Html.fromHtml does not support CSS.
|
||||
*/
|
||||
private fun convertGroupMessageHtml(message: String): String {
|
||||
// Using positive lookbehind to make sure "span" is preceded by "<" or "</"
|
||||
val pattern = "(?<=</?)span".toRegex()
|
||||
|
||||
return message.replace(pattern, "strong")
|
||||
}
|
||||
|
||||
private fun createNotificationItem(
|
||||
notification: Notification,
|
||||
messageText: CharSequence,
|
||||
imageResourceId: Int? = null,
|
||||
textColor: Int? = null
|
||||
): View? {
|
||||
val item = inflater.inflate(R.layout.notification_item, notification_items, false)
|
||||
|
||||
val container = item?.findViewById(R.id.notification_item) as? View
|
||||
container?.setOnClickListener {
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("notificationId", notification.id)
|
||||
setResult(NOTIFICATION_CLICK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
val dismissButton = item?.findViewById(R.id.dismiss_button) as? ImageView
|
||||
dismissButton?.setOnClickListener { viewModel.dismissNotification(notification) }
|
||||
|
||||
val messageTextView = item?.findViewById(R.id.message_text) as? TextView
|
||||
messageTextView?.text = messageText
|
||||
|
||||
if (imageResourceId != null) {
|
||||
val notificationImage = item?.findViewById(R.id.notification_image) as? ImageView
|
||||
notificationImage?.setImageResource(imageResourceId)
|
||||
notificationImage?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (textColor != null) {
|
||||
messageTextView?.setTextColor(ContextCompat.getColor(this, textColor))
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
private fun fromHtml(text: String): CharSequence {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Html.fromHtml(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,15 @@ package com.habitrpg.android.habitica.ui.fragments
|
|||
|
||||
|
||||
import android.app.ActionBar
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.habitrpg.android.habitica.HabiticaBaseApplication
|
||||
|
|
@ -21,6 +25,8 @@ import com.habitrpg.android.habitica.models.inventory.Quest
|
|||
import com.habitrpg.android.habitica.models.inventory.QuestContent
|
||||
import com.habitrpg.android.habitica.models.social.Group
|
||||
import com.habitrpg.android.habitica.ui.activities.MainActivity
|
||||
import com.habitrpg.android.habitica.ui.activities.MainActivity.Companion.NOTIFICATION_CLICK
|
||||
import com.habitrpg.android.habitica.ui.activities.NotificationsActivity
|
||||
import com.habitrpg.android.habitica.ui.adapter.NavigationDrawerAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.social.TavernDetailFragment
|
||||
import com.habitrpg.android.habitica.ui.helpers.NavbarUtils
|
||||
|
|
@ -97,6 +103,7 @@ class NavigationDrawerFragment : DialogFragment() {
|
|||
|
||||
messagesBadge.visibility = View.GONE
|
||||
settingsBadge.visibility = View.GONE
|
||||
notificationsBadge.visibility = View.GONE
|
||||
|
||||
/* Reenable this once the boss art can be displayed correctly.
|
||||
|
||||
|
|
@ -192,6 +199,7 @@ class NavigationDrawerFragment : DialogFragment() {
|
|||
|
||||
messagesButtonWrapper.setOnClickListener { setSelection(R.id.inboxFragment) }
|
||||
settingsButtonWrapper.setOnClickListener { setSelection(R.id.prefsActivity) }
|
||||
notificationsButtonWrapper.setOnClickListener { startNotificationsActivity() }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -245,6 +253,18 @@ class NavigationDrawerFragment : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
fun startNotificationsActivity() {
|
||||
closeDrawer()
|
||||
|
||||
val activity = activity as? MainActivity
|
||||
if (activity != null) {
|
||||
// NotificationsActivity will return a result intent with a notificationId if a
|
||||
// notification item was clicked
|
||||
val intent = Intent(activity, NotificationsActivity::class.java)
|
||||
activity.startActivityForResult(intent, NOTIFICATION_CLICK)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Users of this fragment must call this method to set UP the navigation drawer interactions.
|
||||
*
|
||||
|
|
@ -305,6 +325,24 @@ class NavigationDrawerFragment : DialogFragment() {
|
|||
outState.putInt(STATE_SELECTED_POSITION, mCurrentSelectedPosition)
|
||||
}
|
||||
|
||||
fun setNotificationsCount(unreadNotifications: Int) {
|
||||
if (unreadNotifications == 0) {
|
||||
notificationsBadge.visibility = View.GONE
|
||||
} else {
|
||||
notificationsBadge.visibility = View.VISIBLE
|
||||
notificationsBadge.text = unreadNotifications.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun setNotificationsSeen(allSeen: Boolean) {
|
||||
context.notNull {
|
||||
val colorId = if (allSeen) R.color.gray_200 else R.color.brand_400
|
||||
|
||||
val bg = notificationsBadge.background as GradientDrawable
|
||||
bg.color = ColorStateList.valueOf(ContextCompat.getColor(it, colorId))
|
||||
}
|
||||
}
|
||||
|
||||
fun setMessagesCount(unreadMessages: Int) {
|
||||
if (unreadMessages == 0) {
|
||||
messagesBadge.visibility = View.GONE
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
package com.habitrpg.android.habitica.ui.viewmodels
|
||||
|
||||
import android.os.Bundle
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.components.AppComponent
|
||||
import com.habitrpg.android.habitica.helpers.MainNavigationController
|
||||
import com.habitrpg.android.habitica.helpers.NotificationsManager
|
||||
import com.habitrpg.android.habitica.helpers.RxErrorHandler
|
||||
import com.habitrpg.android.habitica.models.Notification
|
||||
import com.habitrpg.android.habitica.models.notifications.NewChatMessageData
|
||||
import com.habitrpg.android.habitica.models.social.UserParty
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.Consumer
|
||||
import java.util.HashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
open class NotificationsViewModel : BaseViewModel() {
|
||||
@Inject
|
||||
lateinit var notificationsManager: NotificationsManager
|
||||
|
||||
/**
|
||||
* A list of notification types handled by this component.
|
||||
* NOTE: Those not listed here won't be shown in the notification panel
|
||||
*/
|
||||
private val supportedNotificationTypes = listOf(
|
||||
Notification.Type.NEW_STUFF.type,
|
||||
Notification.Type.NEW_CHAT_MESSAGE.type,
|
||||
Notification.Type.NEW_MYSTERY_ITEMS.type,
|
||||
Notification.Type.GROUP_TASK_NEEDS_WORK.type,
|
||||
Notification.Type.GROUP_TASK_APPROVED.type,
|
||||
Notification.Type.UNALLOCATED_STATS_POINTS.type
|
||||
)
|
||||
|
||||
/**
|
||||
* Keep track of users party so we can determine which chat notifications are party chat
|
||||
* instead of guild chat notifications.
|
||||
*/
|
||||
private var party: UserParty? = null
|
||||
|
||||
override fun inject(component: AppComponent) {
|
||||
component.inject(this)
|
||||
}
|
||||
|
||||
init {
|
||||
disposable.add(userRepository.getUser()
|
||||
.subscribe(Consumer {
|
||||
party = it.party
|
||||
}, RxErrorHandler.handleEmptyError()))
|
||||
}
|
||||
|
||||
|
||||
fun getNotifications(): Flowable<List<Notification>> {
|
||||
return notificationsManager.getNotifications()
|
||||
.map { filterSupportedTypes(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun getNotificationCount(): Flowable<Int> {
|
||||
return getNotifications()
|
||||
.map { it.count() }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun allNotificationsSeen(): Flowable<Boolean> {
|
||||
return getNotifications()
|
||||
.map { it.all { notification -> notification.seen == true } }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun refreshNotifications(): Flowable<*> {
|
||||
return userRepository.retrieveUser(withTasks = false, forced = true)
|
||||
}
|
||||
|
||||
private fun filterSupportedTypes(notifications: List<Notification>): List<Notification> {
|
||||
return notifications.filter { supportedNotificationTypes.contains(it.type) }
|
||||
}
|
||||
|
||||
fun isPartyMessage(data: NewChatMessageData?): Boolean {
|
||||
if (party == null || data?.group?.id == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return party?.id == data.group?.id
|
||||
}
|
||||
|
||||
fun dismissNotification(notification: Notification) {
|
||||
disposable.add(userRepository.readNotification(notification.id)
|
||||
.subscribe(Consumer {}, RxErrorHandler.handleEmptyError()))
|
||||
}
|
||||
|
||||
fun dismissAllNotifications(notifications: List<Notification>) {
|
||||
if (notifications.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val notificationIds = HashMap<String, List<String>>()
|
||||
notificationIds["notificationIds"] = notifications.map { notification -> notification.id }
|
||||
|
||||
disposable.add(userRepository.readNotifications(notificationIds)
|
||||
.subscribe(Consumer {}, RxErrorHandler.handleEmptyError()))
|
||||
}
|
||||
|
||||
fun markNotificationsAsSeen(notifications: List<Notification>) {
|
||||
val unseenIds = notifications.filter { notification -> notification.seen != true }
|
||||
.map { notification -> notification.id }
|
||||
|
||||
if (unseenIds.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val notificationIds = HashMap<String, List<String>>()
|
||||
notificationIds["notificationIds"] = unseenIds
|
||||
|
||||
disposable.add(userRepository.seeNotifications(notificationIds)
|
||||
.subscribe(Consumer {}, RxErrorHandler.handleEmptyError()))
|
||||
}
|
||||
|
||||
fun click(notificationId: String, navController: MainNavigationController) {
|
||||
val notification = notificationsManager.getNotification(notificationId) ?: return
|
||||
|
||||
dismissNotification(notification)
|
||||
|
||||
when (notification.type) {
|
||||
Notification.Type.NEW_STUFF.type -> navController.navigate(R.id.newsFragment)
|
||||
Notification.Type.NEW_CHAT_MESSAGE.type -> clickNewChatMessage(notification, navController)
|
||||
Notification.Type.NEW_MYSTERY_ITEMS.type -> navController.navigate(R.id.itemsFragment)
|
||||
Notification.Type.UNALLOCATED_STATS_POINTS.type -> navController.navigate(R.id.statsFragment)
|
||||
// Group tasks should go to Group tasks view if that is added to this app at some point
|
||||
Notification.Type.GROUP_TASK_APPROVED.type -> navController.navigate(R.id.tasksFragment)
|
||||
Notification.Type.GROUP_TASK_NEEDS_WORK.type -> navController.navigate(R.id.tasksFragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clickNewChatMessage(notification: Notification, navController: MainNavigationController) {
|
||||
val data = notification.data as? NewChatMessageData
|
||||
if (isPartyMessage(data)) {
|
||||
navController.navigate(R.id.partyFragment)
|
||||
} else {
|
||||
val bundle = Bundle()
|
||||
bundle.putString("groupID", data?.group?.id)
|
||||
bundle.putBoolean("isMember", true) // safe to assume user is member since they got the notification
|
||||
navController.navigate(R.id.guildFragment, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.habitrpg.android.habitica.utils
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.habitrpg.android.habitica.models.Notification
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class NotificationDeserializer : JsonDeserializer<Notification> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Notification {
|
||||
val notification = Notification()
|
||||
val obj = json.asJsonObject
|
||||
|
||||
if (obj.has("id")) {
|
||||
notification.id = obj.get("id").asString
|
||||
}
|
||||
|
||||
if (obj.has("type")) {
|
||||
notification.type = obj.get("type").asString
|
||||
}
|
||||
|
||||
if (obj.has("seen")) {
|
||||
notification.seen = obj.get("seen").asBoolean
|
||||
}
|
||||
|
||||
val dataType = notification.getDataType()
|
||||
if (obj.has("data") && dataType != null) {
|
||||
notification.data = context.deserialize(obj.getAsJsonObject("data"), dataType)
|
||||
}
|
||||
|
||||
return notification
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
//
|
||||
//import com.habitrpg.android.habitica.data.implementation.ApiClientImpl;
|
||||
//import com.habitrpg.android.habitica.helpers.PopupNotificationsManager;
|
||||
//import com.habitrpg.android.habitica.helpers.NotificationsManager;
|
||||
//import com.habitrpg.android.habitica.proxy.implementation.EmptyCrashlyticsProxy;
|
||||
//import com.habitrpg.android.habitica.data.ApiClient;
|
||||
//import com.habitrpg.android.habitica.BuildConfig;
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
// BuildConfig.PORT,
|
||||
// "",
|
||||
// "");
|
||||
// //apiClient = new ApiClientImpl(ApiClientImpl.createGsonFactory(), hostConfig, new EmptyCrashlyticsProxy(), new PopupNotificationsManager(context), context);
|
||||
// //apiClient = new ApiClientImpl(ApiClientImpl.createGsonFactory(), hostConfig, new EmptyCrashlyticsProxy(), new NotificationsManager(context), context);
|
||||
// //generateUser();
|
||||
// }
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,26 +1,19 @@
|
|||
package com.habitrpg.android.habitica.helpers;
|
||||
|
||||
import com.habitrpg.android.habitica.data.implementation.ApiClientImpl;
|
||||
import com.habitrpg.android.habitica.BuildConfig;
|
||||
import com.habitrpg.android.habitica.HabiticaApplication;
|
||||
import com.habitrpg.android.habitica.api.HostConfig;
|
||||
import com.habitrpg.android.habitica.proxy.implementation.EmptyCrashlyticsProxy;
|
||||
import com.habitrpg.android.habitica.data.ApiClient;
|
||||
import com.habitrpg.android.habitica.models.Notification;
|
||||
import com.habitrpg.android.habitica.models.notifications.NotificationData;
|
||||
import com.habitrpg.android.habitica.models.notifications.LoginIncentiveData;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowAlertDialog;
|
||||
import org.robolectric.shadows.ShadowApplication;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
|
@ -42,7 +35,7 @@ public class PopupNotificationsManagerTest {
|
|||
|
||||
public HostConfig hostConfig;
|
||||
private Context context;
|
||||
private PopupNotificationsManager popupNotificationsManager;
|
||||
private NotificationsManager notificationsManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
|
@ -51,13 +44,13 @@ public class PopupNotificationsManagerTest {
|
|||
BuildConfig.PORT,
|
||||
"",
|
||||
"");
|
||||
popupNotificationsManager =new PopupNotificationsManager(context);
|
||||
notificationsManager =new NotificationsManager(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itDoesNothingWhenNotificationsListIsEmpty() {
|
||||
List<Notification> notifications = new ArrayList<>();
|
||||
popupNotificationsManager.showNotificationDialog(notifications);
|
||||
notificationsManager.handlePopupNotifications(notifications);
|
||||
|
||||
AlertDialog alert =
|
||||
ShadowAlertDialog.getLatestAlertDialog();
|
||||
|
|
@ -74,13 +67,13 @@ public class PopupNotificationsManagerTest {
|
|||
|
||||
notifications.add(notification);
|
||||
|
||||
final PopupNotificationsManager testClass = Mockito.mock(PopupNotificationsManager.class);
|
||||
Mockito.when(testClass.displayNotification(notification)).thenReturn(true);
|
||||
Mockito.when(testClass.showNotificationDialog(notifications)).thenCallRealMethod();
|
||||
final NotificationsManager testClass = Mockito.mock(NotificationsManager.class);
|
||||
Mockito.when(testClass.displayLoginIncentiveNotification(notification)).thenReturn(true);
|
||||
Mockito.when(testClass.handlePopupNotifications(notifications)).thenCallRealMethod();
|
||||
|
||||
testClass.showNotificationDialog(notifications);
|
||||
testClass.handlePopupNotifications(notifications);
|
||||
|
||||
verify(testClass, times(0)).displayNotification(notification);
|
||||
verify(testClass, times(0)).displayLoginIncentiveNotification(notification);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -89,22 +82,22 @@ public class PopupNotificationsManagerTest {
|
|||
|
||||
List<Notification> notifications = new ArrayList<>();
|
||||
|
||||
NotificationData notificationData = new NotificationData();
|
||||
notificationData.message = testTitle;
|
||||
LoginIncentiveData notificationData = new LoginIncentiveData();
|
||||
notificationData.setMessage(testTitle);
|
||||
|
||||
Notification notification = new Notification();
|
||||
notification.setType("LOGIN_INCENTIVE");
|
||||
notification.data = notificationData;
|
||||
notification.setData(notificationData);
|
||||
|
||||
notifications.add(notification);
|
||||
|
||||
final PopupNotificationsManager testClass = Mockito.mock(PopupNotificationsManager.class);
|
||||
Mockito.when(testClass.displayNotification(notification)).thenReturn(true);
|
||||
Mockito.when(testClass.showNotificationDialog(notifications)).thenCallRealMethod();
|
||||
final NotificationsManager testClass = Mockito.mock(NotificationsManager.class);
|
||||
Mockito.when(testClass.displayLoginIncentiveNotification(notification)).thenReturn(true);
|
||||
Mockito.when(testClass.handlePopupNotifications(notifications)).thenCallRealMethod();
|
||||
|
||||
testClass.showNotificationDialog(notifications);
|
||||
testClass.handlePopupNotifications(notifications);
|
||||
|
||||
verify(testClass, times(1)).displayNotification(notification);
|
||||
verify(testClass, times(1)).displayLoginIncentiveNotification(notification);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -113,22 +106,22 @@ public class PopupNotificationsManagerTest {
|
|||
|
||||
List<Notification> notifications = new ArrayList<>();
|
||||
|
||||
NotificationData notificationData = new NotificationData();
|
||||
notificationData.message = testTitle;
|
||||
LoginIncentiveData notificationData = new LoginIncentiveData();
|
||||
notificationData.setMessage(testTitle);
|
||||
|
||||
Notification notification = new Notification();
|
||||
notification.setType("LOGIN_INCENTIVE");
|
||||
notification.data = notificationData;
|
||||
notification.setData(notificationData);
|
||||
|
||||
notifications.add(notification);
|
||||
notifications.add(notification);
|
||||
|
||||
final PopupNotificationsManager testClass = Mockito.mock(PopupNotificationsManager.class);
|
||||
Mockito.when(testClass.displayNotification(notification)).thenReturn(true);
|
||||
Mockito.when(testClass.showNotificationDialog(notifications)).thenCallRealMethod();
|
||||
final NotificationsManager testClass = Mockito.mock(NotificationsManager.class);
|
||||
Mockito.when(testClass.displayLoginIncentiveNotification(notification)).thenReturn(true);
|
||||
Mockito.when(testClass.handlePopupNotifications(notifications)).thenCallRealMethod();
|
||||
|
||||
testClass.showNotificationDialog(notifications);
|
||||
testClass.handlePopupNotifications(notifications);
|
||||
|
||||
verify(testClass, times(1)).displayNotification(notification);
|
||||
verify(testClass, times(1)).displayLoginIncentiveNotification(notification);
|
||||
}
|
||||
}
|
||||
|
|
|
|||