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
This commit is contained in:
Phillip Thelen 2019-06-03 12:36:33 +02:00
commit 73114f14f1
60 changed files with 2529 additions and 1668 deletions

View file

@ -64,6 +64,13 @@
android:pathPattern="/settings/.*"/> android:pathPattern="/settings/.*"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.activities.NotificationsActivity"
android:parentActivityName=".ui.activities.MainActivity"
android:label="@string/notifications"
android:screenOrientation="portrait"
tools:ignore="UnusedAttribute">
</activity>
<activity <activity
android:name=".ui.activities.FixCharacterValuesActivity" android:name=".ui.activities.FixCharacterValuesActivity"
android:parentActivityName=".ui.activities.PrefsActivity" android:parentActivityName=".ui.activities.PrefsActivity"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -4,10 +4,5 @@
<corners <corners
android:radius="10dip"/> android:radius="10dip"/>
<solid <solid
android:color="@color/red_50" /> android:color="@color/brand_400" />
<padding
android:left="5dip"
android:right="5dip"
android:top="5dip"
android:bottom="5dip" />
</shape> </shape>

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

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

View file

@ -12,7 +12,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64dp" android:layout_height="64dp"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingRight="16dp" android:paddingRight="1dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
@ -58,10 +58,42 @@
</LinearLayout> </LinearLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/messagesButtonWrapper" android:id="@+id/notificationsButtonWrapper"
android:layout_width="45dp" android:layout_width="33dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginLeft="16dp"> 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 <ImageView
android:id="@+id/messagesButton" android:id="@+id/messagesButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -70,21 +102,24 @@
android:src="@drawable/menu_messages" android:src="@drawable/menu_messages"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:clickable="false" /> android:clickable="false" />
<TextView <TextView
android:id="@+id/messagesBadge" android:id="@+id/messagesBadge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="20dp" android:layout_height="20dp"
android:paddingTop="0dp"
android:layout_alignTop="@id/messagesButton"
android:layout_alignLeft="@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:textColor="#FFF"
android:textSize="12sp" android:textSize="12sp"
android:background="@drawable/badge_circle"
android:layout_marginTop="-12dp"
android:layout_marginLeft="13dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"/> tools:text="1"
tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/settingsButtonWrapper" android:id="@+id/settingsButtonWrapper"
@ -98,21 +133,25 @@
android:background="@color/transparent" android:background="@color/transparent"
android:src="@drawable/menu_settings" android:src="@drawable/menu_settings"
android:layout_centerVertical="true"/> android:layout_centerVertical="true"/>
<TextView <TextView
android:id="@+id/settingsBadge" android:id="@+id/settingsBadge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="20dp" android:layout_height="20dp"
android:paddingTop="0dp"
android:layout_alignTop="@id/settingsButton"
android:layout_alignLeft="@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:textColor="#FFF"
android:textSize="12sp" android:textSize="12sp"
android:background="@drawable/badge_circle"
android:layout_marginTop="-12dp"
android:layout_marginLeft="13dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"/> tools:text="1"
tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

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

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

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

View file

@ -210,6 +210,11 @@
app:argType="string" app:argType="string"
app:nullable="true" /> app:nullable="true" />
</fragment> </fragment>
<activity
android:id="@+id/notificationsActivity"
android:name="com.habitrpg.android.habitica.ui.activities.NotificationsActivity"
android:label="@string/notifications">
</activity>
<fragment <fragment
android:id="@+id/petDetailRecyclerFragment" android:id="@+id/petDetailRecyclerFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.stable.PetDetailRecyclerFragment" android:name="com.habitrpg.android.habitica.ui.fragments.inventory.stable.PetDetailRecyclerFragment"

View file

@ -534,6 +534,7 @@
<string name="open_settings">Open Settings</string> <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="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="inbox">Messages</string>
<string name="notifications">Notifications</string>
<string name="FAQ">Frequently Asked Questions</string> <string name="FAQ">Frequently Asked Questions</string>
<string name="special">Special</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> <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="discover">Discover</string>
<string name="damage_paused">Damage paused</string> <string name="damage_paused">Damage paused</string>
<string name="preference_push_important_announcements">Important Announcements</string> <string name="preference_push_important_announcements">Important Announcements</string>
<string name="no_notifications_title">Youre 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="create">Create</string>
<string name="only_leader_create_challenge">Only leader can create Challenges</string> <string name="only_leader_create_challenge">Only leader can create Challenges</string>
<string name="create_party">Create Party</string> <string name="create_party">Create Party</string>

View file

@ -348,6 +348,12 @@ public interface ApiService {
@POST("notifications/{notificationId}/read") @POST("notifications/{notificationId}/read")
Flowable<HabitResponse<List>> readNotification(@Path("notificationId") String notificationId); 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") @POST("user/open-mystery-item")
Flowable<HabitResponse<Equipment>> openMysteryItem(); Flowable<HabitResponse<Equipment>> openMysteryItem();

View file

@ -6,6 +6,7 @@ import com.google.gson.reflect.TypeToken;
import com.habitrpg.android.habitica.models.Achievement; import com.habitrpg.android.habitica.models.Achievement;
import com.habitrpg.android.habitica.models.ContentResult; import com.habitrpg.android.habitica.models.ContentResult;
import com.habitrpg.android.habitica.models.FAQArticle; 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.Skill;
import com.habitrpg.android.habitica.models.Tag; import com.habitrpg.android.habitica.models.Tag;
import com.habitrpg.android.habitica.models.TutorialStep; 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.MemberSerialization;
import com.habitrpg.android.habitica.utils.OwnedItemListDeserializer; import com.habitrpg.android.habitica.utils.OwnedItemListDeserializer;
import com.habitrpg.android.habitica.utils.OwnedMountListDeserializer; 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.OwnedPetListDeserializer;
import com.habitrpg.android.habitica.utils.PurchasedDeserializer; import com.habitrpg.android.habitica.utils.PurchasedDeserializer;
import com.habitrpg.android.habitica.utils.QuestCollectDeserializer; import com.habitrpg.android.habitica.utils.QuestCollectDeserializer;
@ -118,6 +120,7 @@ public class GSonFactoryCreator {
.registerTypeAdapter(Member.class, new MemberSerialization()) .registerTypeAdapter(Member.class, new MemberSerialization())
.registerTypeAdapter(WorldState.class, new WorldStateSerialization()) .registerTypeAdapter(WorldState.class, new WorldStateSerialization())
.registerTypeAdapter(FindUsernameResult.class, new FindUsernameResultDeserializer()) .registerTypeAdapter(FindUsernameResult.class, new FindUsernameResultDeserializer())
.registerTypeAdapter(Notification.class, new NotificationDeserializer())
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.create(); .create();
return GsonConverterFactory.create(gson); return GsonConverterFactory.create(gson);

View file

@ -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.LoginActivity;
import com.habitrpg.android.habitica.ui.activities.MainActivity; import com.habitrpg.android.habitica.ui.activities.MainActivity;
import com.habitrpg.android.habitica.ui.activities.MaintenanceActivity; 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.GroupInviteActivity;
import com.habitrpg.android.habitica.ui.activities.PrefsActivity; import com.habitrpg.android.habitica.ui.activities.PrefsActivity;
import com.habitrpg.android.habitica.ui.activities.ReportMessageActivity; 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.TaskRecyclerViewFragment;
import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment; import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment;
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel; 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.social.ChatBarView;
import com.habitrpg.android.habitica.ui.views.stats.BulkAllocateStatsDialog; import com.habitrpg.android.habitica.ui.views.stats.BulkAllocateStatsDialog;
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog; import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog;
@ -134,6 +136,8 @@ public interface AppComponent {
void inject(PrefsActivity prefsActivity); void inject(PrefsActivity prefsActivity);
void inject(NotificationsActivity notificationsActivity);
void inject(SetupActivity setupActivity); void inject(SetupActivity setupActivity);
void inject(SkillTasksActivity skillTasksActivity); void inject(SkillTasksActivity skillTasksActivity);
@ -302,6 +306,8 @@ public interface AppComponent {
void inject(@NotNull GroupViewModel viewModel); void inject(@NotNull GroupViewModel viewModel);
void inject(@NotNull NotificationsViewModel viewModel);
void inject(@NotNull ChatFragment chatFragment); void inject(@NotNull ChatFragment chatFragment);
void inject(@NotNull GiftIAPActivity giftIAPActivity); void inject(@NotNull GiftIAPActivity giftIAPActivity);

View file

@ -212,6 +212,8 @@ interface ApiClient {
// Notifications // Notifications
fun readNotification(notificationId: String): Flowable<List<*>> 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 fun getErrorResponse(throwable: HttpException): ErrorResponse

View file

@ -52,6 +52,8 @@ interface UserRepository : BaseRepository {
fun runCron() fun runCron()
fun readNotification(id: String): Flowable<List<*>> 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> fun changeCustomDayStart(dayStartTime: Int): Flowable<User>

View file

@ -172,6 +172,12 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
override fun readNotification(id: String): Flowable<List<*>> = apiClient.readNotification(id) 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> { override fun changeCustomDayStart(dayStartTime: Int): Flowable<User> {
val updateObject = HashMap<String, Any>() val updateObject = HashMap<String, Any>()
updateObject["dayStart"] = dayStartTime updateObject["dayStart"] = dayStartTime

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
package com.habitrpg.android.habitica.models.notifications
open class GroupTaskApprovedData : NotificationData {
var groupId: String = ""
var message: String? = null
}

View file

@ -0,0 +1,7 @@
package com.habitrpg.android.habitica.models.notifications
open class GroupTaskNeedsWorkData : NotificationData {
var group: NotificationGroup? = null
var message: String? = null
}

View file

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

View file

@ -0,0 +1,5 @@
package com.habitrpg.android.habitica.models.notifications
open class NewChatMessageData : NotificationData {
var group: NotificationGroup? = null
}

View file

@ -0,0 +1,5 @@
package com.habitrpg.android.habitica.models.notifications
open class NewStuffData : NotificationData {
var title: String? = null
}

View file

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

View file

@ -0,0 +1,3 @@
package com.habitrpg.android.habitica.models.notifications
interface NotificationData

View file

@ -0,0 +1,7 @@
package com.habitrpg.android.habitica.models.notifications
open class NotificationGroup {
var id: String = ""
var name: String? = null
}

View file

@ -0,0 +1,5 @@
package com.habitrpg.android.habitica.models.notifications
open class UnallocatedPointsData : NotificationData {
var points: Int? = null
}

View file

@ -7,8 +7,8 @@ import com.habitrpg.android.habitica.api.HostConfig;
import com.habitrpg.android.habitica.api.MaintenanceApiService; import com.habitrpg.android.habitica.api.MaintenanceApiService;
import com.habitrpg.android.habitica.data.ApiClient; import com.habitrpg.android.habitica.data.ApiClient;
import com.habitrpg.android.habitica.data.implementation.ApiClientImpl; 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.KeyHelper;
import com.habitrpg.android.habitica.helpers.PopupNotificationsManager;
import com.habitrpg.android.habitica.proxy.CrashlyticsProxy; import com.habitrpg.android.habitica.proxy.CrashlyticsProxy;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -36,14 +36,14 @@ public class ApiModule {
@Provides @Provides
@Singleton @Singleton
public PopupNotificationsManager providesPopupNotificationsManager(Context context) { public NotificationsManager providesPopupNotificationsManager(Context context) {
return new PopupNotificationsManager(context); return new NotificationsManager(context);
} }
@Provides @Provides
@Singleton @Singleton
public ApiClient providesApiHelper(GsonConverterFactory gsonConverter, HostConfig hostConfig, CrashlyticsProxy crashlyticsProxy, PopupNotificationsManager popupNotificationsManager, Context context) { public ApiClient providesApiHelper(GsonConverterFactory gsonConverter, HostConfig hostConfig, CrashlyticsProxy crashlyticsProxy, NotificationsManager notificationsManager, Context context) {
return new ApiClientImpl(gsonConverter, hostConfig, crashlyticsProxy, popupNotificationsManager, context); return new ApiClientImpl(gsonConverter, hostConfig, crashlyticsProxy, notificationsManager, context);
} }
@Provides @Provides

View file

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

View file

@ -2,11 +2,15 @@ package com.habitrpg.android.habitica.ui.fragments
import android.app.ActionBar import android.app.ActionBar
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.drawable.GradientDrawable
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.habitrpg.android.habitica.HabiticaBaseApplication 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.inventory.QuestContent
import com.habitrpg.android.habitica.models.social.Group import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.ui.activities.MainActivity 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.adapter.NavigationDrawerAdapter
import com.habitrpg.android.habitica.ui.fragments.social.TavernDetailFragment import com.habitrpg.android.habitica.ui.fragments.social.TavernDetailFragment
import com.habitrpg.android.habitica.ui.helpers.NavbarUtils import com.habitrpg.android.habitica.ui.helpers.NavbarUtils
@ -97,6 +103,7 @@ class NavigationDrawerFragment : DialogFragment() {
messagesBadge.visibility = View.GONE messagesBadge.visibility = View.GONE
settingsBadge.visibility = View.GONE settingsBadge.visibility = View.GONE
notificationsBadge.visibility = View.GONE
/* Reenable this once the boss art can be displayed correctly. /* Reenable this once the boss art can be displayed correctly.
@ -192,6 +199,7 @@ class NavigationDrawerFragment : DialogFragment() {
messagesButtonWrapper.setOnClickListener { setSelection(R.id.inboxFragment) } messagesButtonWrapper.setOnClickListener { setSelection(R.id.inboxFragment) }
settingsButtonWrapper.setOnClickListener { setSelection(R.id.prefsActivity) } settingsButtonWrapper.setOnClickListener { setSelection(R.id.prefsActivity) }
notificationsButtonWrapper.setOnClickListener { startNotificationsActivity() }
} }
override fun onDestroy() { 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. * 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) 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) { fun setMessagesCount(unreadMessages: Int) {
if (unreadMessages == 0) { if (unreadMessages == 0) {
messagesBadge.visibility = View.GONE messagesBadge.visibility = View.GONE

View file

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

View file

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

View file

@ -2,7 +2,7 @@
// //
// //
//import com.habitrpg.android.habitica.data.implementation.ApiClientImpl; //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.proxy.implementation.EmptyCrashlyticsProxy;
//import com.habitrpg.android.habitica.data.ApiClient; //import com.habitrpg.android.habitica.data.ApiClient;
//import com.habitrpg.android.habitica.BuildConfig; //import com.habitrpg.android.habitica.BuildConfig;
@ -62,7 +62,7 @@
// BuildConfig.PORT, // 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(); // //generateUser();
// } // }
// //

View file

@ -1,26 +1,19 @@
package com.habitrpg.android.habitica.helpers; 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.BuildConfig;
import com.habitrpg.android.habitica.HabiticaApplication;
import com.habitrpg.android.habitica.api.HostConfig; 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.Notification;
import com.habitrpg.android.habitica.models.notifications.NotificationData; import com.habitrpg.android.habitica.models.notifications.LoginIncentiveData;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowAlertDialog; import org.robolectric.shadows.ShadowAlertDialog;
import org.robolectric.shadows.ShadowApplication;
import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
@ -42,7 +35,7 @@ public class PopupNotificationsManagerTest {
public HostConfig hostConfig; public HostConfig hostConfig;
private Context context; private Context context;
private PopupNotificationsManager popupNotificationsManager; private NotificationsManager notificationsManager;
@Before @Before
public void setUp() { public void setUp() {
@ -51,13 +44,13 @@ public class PopupNotificationsManagerTest {
BuildConfig.PORT, BuildConfig.PORT,
"", "",
""); "");
popupNotificationsManager =new PopupNotificationsManager(context); notificationsManager =new NotificationsManager(context);
} }
@Test @Test
public void itDoesNothingWhenNotificationsListIsEmpty() { public void itDoesNothingWhenNotificationsListIsEmpty() {
List<Notification> notifications = new ArrayList<>(); List<Notification> notifications = new ArrayList<>();
popupNotificationsManager.showNotificationDialog(notifications); notificationsManager.handlePopupNotifications(notifications);
AlertDialog alert = AlertDialog alert =
ShadowAlertDialog.getLatestAlertDialog(); ShadowAlertDialog.getLatestAlertDialog();
@ -74,13 +67,13 @@ public class PopupNotificationsManagerTest {
notifications.add(notification); notifications.add(notification);
final PopupNotificationsManager testClass = Mockito.mock(PopupNotificationsManager.class); final NotificationsManager testClass = Mockito.mock(NotificationsManager.class);
Mockito.when(testClass.displayNotification(notification)).thenReturn(true); Mockito.when(testClass.displayLoginIncentiveNotification(notification)).thenReturn(true);
Mockito.when(testClass.showNotificationDialog(notifications)).thenCallRealMethod(); 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 @Test
@ -89,22 +82,22 @@ public class PopupNotificationsManagerTest {
List<Notification> notifications = new ArrayList<>(); List<Notification> notifications = new ArrayList<>();
NotificationData notificationData = new NotificationData(); LoginIncentiveData notificationData = new LoginIncentiveData();
notificationData.message = testTitle; notificationData.setMessage(testTitle);
Notification notification = new Notification(); Notification notification = new Notification();
notification.setType("LOGIN_INCENTIVE"); notification.setType("LOGIN_INCENTIVE");
notification.data = notificationData; notification.setData(notificationData);
notifications.add(notification); notifications.add(notification);
final PopupNotificationsManager testClass = Mockito.mock(PopupNotificationsManager.class); final NotificationsManager testClass = Mockito.mock(NotificationsManager.class);
Mockito.when(testClass.displayNotification(notification)).thenReturn(true); Mockito.when(testClass.displayLoginIncentiveNotification(notification)).thenReturn(true);
Mockito.when(testClass.showNotificationDialog(notifications)).thenCallRealMethod(); 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 @Test
@ -113,22 +106,22 @@ public class PopupNotificationsManagerTest {
List<Notification> notifications = new ArrayList<>(); List<Notification> notifications = new ArrayList<>();
NotificationData notificationData = new NotificationData(); LoginIncentiveData notificationData = new LoginIncentiveData();
notificationData.message = testTitle; notificationData.setMessage(testTitle);
Notification notification = new Notification(); Notification notification = new Notification();
notification.setType("LOGIN_INCENTIVE"); notification.setType("LOGIN_INCENTIVE");
notification.data = notificationData; notification.setData(notificationData);
notifications.add(notification); notifications.add(notification);
notifications.add(notification); notifications.add(notification);
final PopupNotificationsManager testClass = Mockito.mock(PopupNotificationsManager.class); final NotificationsManager testClass = Mockito.mock(NotificationsManager.class);
Mockito.when(testClass.displayNotification(notification)).thenReturn(true); Mockito.when(testClass.displayLoginIncentiveNotification(notification)).thenReturn(true);
Mockito.when(testClass.showNotificationDialog(notifications)).thenCallRealMethod(); Mockito.when(testClass.handlePopupNotifications(notifications)).thenCallRealMethod();
testClass.showNotificationDialog(notifications); testClass.handlePopupNotifications(notifications);
verify(testClass, times(1)).displayNotification(notification); verify(testClass, times(1)).displayLoginIncentiveNotification(notification);
} }
} }