From 4d024c06cc5bbf5104692679f58ab08e1f5e4165 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Tue, 18 Aug 2020 14:19:35 +0100 Subject: [PATCH] In-app notifications (#969) * display notification bell icon beside URL bar * notification model * persist received notifications * Refresh notification list. Add is_read flag and display unread count. * Hide notifications when opening the navigatinon drawer. Open selected notification item before hiding the list. * update local store with remote notifications * use card view for notification items * make notification cards clickable * handle comment hash * fix remote notifications not loading. fix comment scroll. * Improve in-app comment notification linking. Add icons. --- app/build.gradle | 4 +- .../lbry/browser/LbrynetMessagingService.java | 50 ++++- .../java/io/lbry/browser/MainActivity.java | 183 +++++++++++++++++- .../browser/adapter/ClaimListAdapter.java | 17 +- .../browser/adapter/CommentListAdapter.java | 10 + .../adapter/NotificationListAdapter.java | 136 +++++++++++++ .../io/lbry/browser/data/DatabaseHelper.java | 119 +++++++++++- .../java/io/lbry/browser/model/Claim.java | 1 + .../model/lbryinc/LbryNotification.java | 19 ++ .../lbry/browser/tasks/CommentListTask.java | 4 +- .../tasks/lbryinc/NotificationListTask.java | 120 ++++++++++++ .../ui/findcontent/FileViewFragment.java | 83 ++++++-- .../java/io/lbry/browser/utils/Helper.java | 1 + .../res/drawable-anydpi/ic_notifications.xml | 11 ++ .../res/drawable-hdpi/ic_notifications.png | Bin 0 -> 273 bytes .../res/drawable-mdpi/ic_notifications.png | Bin 0 -> 203 bytes .../res/drawable-xhdpi/ic_notifications.png | Bin 0 -> 333 bytes .../res/drawable-xxhdpi/ic_notifications.png | Bin 0 -> 477 bytes .../res/drawable/bg_notification_badge.xml | 6 + app/src/main/res/layout/app_bar_main.xml | 40 +++- app/src/main/res/layout/content_main.xml | 47 +++++ .../main/res/layout/fragment_file_view.xml | 13 -- .../res/layout/list_item_notification.xml | 50 +++++ app/src/main/res/layout/list_item_stream.xml | 23 +++ app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/settings.xml | 6 + 26 files changed, 892 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/io/lbry/browser/adapter/NotificationListAdapter.java create mode 100644 app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/lbryinc/NotificationListTask.java create mode 100644 app/src/main/res/drawable-anydpi/ic_notifications.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_notifications.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notifications.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notifications.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notifications.png create mode 100644 app/src/main/res/drawable/bg_notification_badge.xml create mode 100644 app/src/main/res/layout/list_item_notification.xml diff --git a/app/build.gradle b/app/build.gradle index 6c239c8f..3b8e12de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,8 +101,8 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - __32bitImplementation 'io.lbry:lbrysdk32:0.79.1' - __64bitImplementation 'io.lbry:lbrysdk64:0.79.1' + __32bitImplementation 'io.lbry:lbrysdk32:0.80.0' + __64bitImplementation 'io.lbry:lbrysdk64:0.80.0' } apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java b/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java index 62536467..aa8f1fe6 100644 --- a/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java +++ b/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java @@ -6,6 +6,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.database.sqlite.SQLiteDatabase; import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; @@ -20,19 +21,22 @@ import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; +import io.lbry.browser.data.DatabaseHelper; +import io.lbry.browser.model.lbryinc.LbryNotification; +import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.LbryAnalytics; -import io.lbry.lbrysdk.LbrynetService; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; public class LbrynetMessagingService extends FirebaseMessagingService { + public static final String ACTION_NOTIFICATION_RECEIVED = "io.lbry.browser.Broadcast.NotificationReceived"; private static final String TAG = "LbrynetMessagingService"; private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL"; + private static final String TYPE_COMMENT = "comment"; private static final String TYPE_SUBSCRIPTION = "subscription"; private static final String TYPE_REWARD = "reward"; private static final String TYPE_INTERESTS = "interests"; @@ -52,10 +56,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService { String title = payload.get("title"); String body = payload.get("body"); String name = payload.get("name"); // notification name - String contentTitle = payload.get("content_title"); - String channelUrl = payload.get("channel_url"); - //String publishTime = payload.get("publish_time"); - String publishTime = null; + String hash = payload.get("hash"); // comment hash if (type != null && getEnabledTypes().indexOf(type) > -1 && body != null && body.trim().length() > 0) { // only log the receive event for valid notifications received @@ -65,7 +66,34 @@ public class LbrynetMessagingService extends FirebaseMessagingService { firebaseAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_RECEIVE, bundle); } - sendNotification(title, body, type, url, name, contentTitle, channelUrl, publishTime); + if (!Helper.isNullOrEmpty(hash)) { + url = String.format("%s?comment_hash=%s", url, hash); + } + + sendNotification(title, body, type, url, name); + } + + // persist the notification data + try { + DatabaseHelper helper = DatabaseHelper.getInstance(); + SQLiteDatabase db = helper.getWritableDatabase(); + LbryNotification lnotification = new LbryNotification(); + lnotification.setTitle(title); + lnotification.setDescription(body); + lnotification.setTargetUrl(url); + lnotification.setTimestamp(new Date()); + DatabaseHelper.createOrUpdateNotification(lnotification, db); + + // send a broadcast + Intent intent = new Intent(ACTION_NOTIFICATION_RECEIVED); + intent.putExtra("title", title); + intent.putExtra("body", body); + intent.putExtra("url", url); + intent.putExtra("timestamp", lnotification.getTimestamp().getTime()); + sendBroadcast(intent); + } catch (Exception ex) { + // don't fail if any error occurs while saving a notification + Log.e(TAG, "could not save notification", ex); } } } @@ -97,8 +125,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService { * * @param messageBody FCM message body received. */ - private void sendNotification(String title, String messageBody, String type, String url, String name, - String contentTitle, String channelUrl, String publishTime) { + private void sendNotification(String title, String messageBody, String type, String url, String name) { //Intent intent = new Intent(this, MainActivity.class); //intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (url == null) { @@ -143,6 +170,9 @@ public class LbrynetMessagingService extends FirebaseMessagingService { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); List enabledTypes = new ArrayList(); + if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_COMMENTS, true)) { + enabledTypes.add(TYPE_COMMENT); + } if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS, true)) { enabledTypes.add(TYPE_SUBSCRIPTION); } diff --git a/app/src/main/java/io/lbry/browser/MainActivity.java b/app/src/main/java/io/lbry/browser/MainActivity.java index ab486665..3d80e379 100644 --- a/app/src/main/java/io/lbry/browser/MainActivity.java +++ b/app/src/main/java/io/lbry/browser/MainActivity.java @@ -115,6 +115,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import io.lbry.browser.adapter.NavigationMenuAdapter; +import io.lbry.browser.adapter.NotificationListAdapter; import io.lbry.browser.adapter.UrlSuggestionListAdapter; import io.lbry.browser.data.DatabaseHelper; import io.lbry.browser.dialog.ContentScopeDialogFragment; @@ -137,6 +138,7 @@ import io.lbry.browser.model.Tag; import io.lbry.browser.model.UrlSuggestion; import io.lbry.browser.model.WalletBalance; import io.lbry.browser.model.WalletSync; +import io.lbry.browser.model.lbryinc.LbryNotification; import io.lbry.browser.model.lbryinc.Reward; import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.tasks.GenericTaskHandler; @@ -147,6 +149,7 @@ import io.lbry.browser.tasks.lbryinc.FetchRewardsTask; import io.lbry.browser.tasks.LighthouseAutoCompleteTask; import io.lbry.browser.tasks.MergeSubscriptionsTask; import io.lbry.browser.tasks.claim.ResolveTask; +import io.lbry.browser.tasks.lbryinc.NotificationListTask; import io.lbry.browser.tasks.localdata.FetchRecentUrlHistoryTask; import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler; import io.lbry.browser.tasks.wallet.LoadSharedUserStateTask; @@ -224,6 +227,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener @Getter private String firebaseMessagingToken; + private NotificationListAdapter notificationListAdapter; + private Map openNavFragments; private static final Map fragmentClassNavIdMap = new HashMap<>(); static { @@ -282,6 +287,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public static final String PREFERENCE_KEY_DARK_MODE = "io.lbry.browser.preference.userinterface.DarkMode"; public static final String PREFERENCE_KEY_SHOW_MATURE_CONTENT = "io.lbry.browser.preference.userinterface.ShowMatureContent"; public static final String PREFERENCE_KEY_SHOW_URL_SUGGESTIONS = "io.lbry.browser.preference.userinterface.UrlSuggestions"; + public static final String PREFERENCE_KEY_NOTIFICATION_COMMENTS = "io.lbry.browser.preference.notifications.Comments"; public static final String PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS = "io.lbry.browser.preference.notifications.Subscriptions"; public static final String PREFERENCE_KEY_NOTIFICATION_REWARDS = "io.lbry.browser.preference.notifications.Rewards"; public static final String PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS = "io.lbry.browser.preference.notifications.ContentInterests"; @@ -439,6 +445,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener // setup uri bar setupUriBar(); + initNotificationsPage(); + loadUnreadNotificationsCount(); // other pendingSyncSetQueue = new ArrayList<>(); @@ -475,6 +483,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public void onDrawerSlide(View drawerView, float slideOffset) { if (slideOffset != 0) { clearWunderbarFocus(findViewById(R.id.wunderbar)); + hideNotifications(); } super.onDrawerSlide(drawerView, slideOffset); } @@ -503,10 +512,23 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } }); + findViewById(R.id.wunderbar_notifications).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + View container = findViewById(R.id.notifications_container); + if (container.getVisibility() != View.VISIBLE) { + showNotifications(); + } else { + hideNotifications(); + } + } + }); + findViewById(R.id.global_now_playing_card).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) { + hideNotifications(); openFileUrl(nowPlayingClaimUrl); } } @@ -551,7 +573,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public boolean isBackgroundPlaybackEnabled() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - return sp.getBoolean(PREFERENCE_KEY_BACKGROUND_PLAYBACK, false); + return sp.getBoolean(PREFERENCE_KEY_BACKGROUND_PLAYBACK, true); } public boolean initialSubscriptionMergeDone() { @@ -717,8 +739,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } private void openSelectedMenuItem() { + hideNotifications(); + switch (selectedMenuItemId) { - // TODO: reverse map lookup for class? case NavMenuItem.ID_ITEM_FOLLOWING: openFragment(FollowingFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING); break; @@ -1067,7 +1090,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener View container = findViewById(R.id.url_suggestions_container); View closeIcon = findViewById(R.id.wunderbar_close); EditText wunderbar = findViewById(R.id.wunderbar); - wunderbar.setPadding(0, 0, visible ? getScaledValue(36) : 0, 0); + //wunderbar.setPadding(0, 0, visible ? getScaledValue(36) : 0, 0); container.setVisibility(visible ? View.VISIBLE : View.GONE); closeIcon.setVisibility(visible ? View.VISIBLE : View.GONE); @@ -1102,8 +1125,13 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener @Override public void onFocusChange(View view, boolean hasFocus) { if (hasFocus) { + hideNotifications(); + findViewById(R.id.wunderbar_notifications).setVisibility(View.INVISIBLE); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(view, 0); + } else { + findViewById(R.id.wunderbar_notifications).setVisibility(View.VISIBLE); } if (canShowUrlSuggestions()) { @@ -1636,6 +1664,14 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener private static final String CHANNEL_ID_PLAYBACK = "io.lbry.browser.LBRY_PLAYBACK_CHANNEL"; private static final int PLAYBACK_NOTIFICATION_ID = 3; + public void initNotificationsPage() { + findViewById(R.id.notification_list_empty_container).setVisibility(View.VISIBLE); + + RecyclerView notificationsList = findViewById(R.id.notifications_list); + LinearLayoutManager llm = new LinearLayoutManager(this); + notificationsList.setLayoutManager(llm); + } + public void initPlaybackNotification() { if (isBackgroundPlaybackEnabled()) { playerNotificationManager.setPlayer(MainActivity.appPlayer); @@ -2006,6 +2042,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener intentFilter.addAction(ACTION_OPEN_REWARDS_PAGE); intentFilter.addAction(ACTION_PUBLISH_SUCCESSFUL); intentFilter.addAction(ACTION_SAVE_SHARED_USER_STATE); + intentFilter.addAction(LbrynetMessagingService.ACTION_NOTIFICATION_RECEIVED); requestsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -2026,6 +2063,25 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener saveSharedUserState(); } else if (ACTION_PUBLISH_SUCCESSFUL.equalsIgnoreCase(action)) { openPublishesOnSuccessfulPublish(); + } else if (LbrynetMessagingService.ACTION_NOTIFICATION_RECEIVED.equalsIgnoreCase(action)) { + handleNotificationReceived(intent); + } + } + + private void handleNotificationReceived(Intent intent) { + loadUnreadNotificationsCount(); + + if (notificationListAdapter != null) { + LbryNotification lnotification = new LbryNotification(); + lnotification.setTitle(intent.getStringExtra("title")); + lnotification.setDescription(intent.getStringExtra("body")); + lnotification.setTargetUrl(intent.getStringExtra("url")); + lnotification.setTimestamp(new Date(intent.getLongExtra("timestamp", System.currentTimeMillis()))); + // show at the top + notificationListAdapter.insertNotification(lnotification, 0); + findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE); + } else { + loadRemoteNotifications(); } } @@ -2071,13 +2127,48 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); } + public void showNotifications() { + clearWunderbarFocus(findViewById(R.id.wunderbar)); + findViewById(R.id.notifications_container).setVisibility(View.VISIBLE); + ((ImageView) findViewById(R.id.notifications_toggle_icon)).setColorFilter(ContextCompat.getColor(this, R.color.lbryGreen)); + if (notificationListAdapter == null) { + loadRemoteNotifications(); + } + markNotificationsRead(); + } + + public void hideNotifications() { + ((ImageView) findViewById(R.id.notifications_toggle_icon)).setColorFilter(ContextCompat.getColor(this, R.color.actionBarForeground)); + findViewById(R.id.notifications_container).setVisibility(View.GONE); + } + + private void markNotificationsRead() { + (new AsyncTask() { + protected Void doInBackground(Void... params) { + try { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + DatabaseHelper.markNotificationsRead(db); + } catch (Exception ex) { + // pass + } + return null; + } + protected void onPostExecute(Void result) { + loadUnreadNotificationsCount(); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + @Override public void onBackPressed() { - if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) { clearWunderbarFocus(findViewById(R.id.wunderbar)); return; } + if (findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) { + hideNotifications(); + return; + } if (backPressInterceptor != null && backPressInterceptor.onBackPressed()) { return; } @@ -3051,6 +3142,90 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + private void displayUnreadNotificationCount(int count) { + String text = count > 99 ? "99+" : String.valueOf(count); + + TextView badge = findViewById(R.id.notifications_badge_count); + badge.setVisibility(count > 0 ? View.VISIBLE : View.INVISIBLE); + badge.setText(text); + } + + private void loadUnreadNotificationsCount() { + (new AsyncTask() { + @Override + protected Integer doInBackground(Void... params) { + try { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + return DatabaseHelper.getUnreadNotificationsCount(db); + } catch (Exception ex) { + return 0; + } + } + protected void onPostExecute(Integer count) { + displayUnreadNotificationCount(count); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void loadRemoteNotifications() { + findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE); + NotificationListTask task = new NotificationListTask(this, findViewById(R.id.notifications_progress), new NotificationListTask.ListNotificationsHandler() { + @Override + public void onSuccess(List notifications) { + loadLocalNotifications(); + } + + @Override + public void onError(Exception exception) { + // pass + Log.e(TAG, "error loading remote notifications", exception); + loadLocalNotifications(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void loadLocalNotifications() { + (new AsyncTask>() { + protected void onPreExecute() { + findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE); + findViewById(R.id.notifications_progress).setVisibility(View.VISIBLE); + } + @Override + protected List doInBackground(Void... params) { + List notifications = new ArrayList<>(); + try { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + notifications = DatabaseHelper.getNotifications(db); + } catch (Exception ex) { + // pass + } + return notifications; + } + protected void onPostExecute(List notifications) { + findViewById(R.id.notification_list_empty_container).setVisibility(notifications.size() == 0 ? View.VISIBLE : View.GONE); + findViewById(R.id.notifications_progress).setVisibility(View.GONE); + notificationListAdapter = new NotificationListAdapter(notifications, MainActivity.this); + notificationListAdapter.setClickListener(new NotificationListAdapter.NotificationClickListener() { + @Override + public void onNotificationClicked(LbryNotification notification) { + LbryUri target = LbryUri.tryParse(notification.getTargetUrl()); + if (target != null) { + if (target.isChannel()) { + openChannelUrl(notification.getTargetUrl()); + } else { + openFileUrl(notification.getTargetUrl()); + } + hideNotifications(); + } + } + }); + + ((RecyclerView) findViewById(R.id.notifications_list)).setAdapter(notificationListAdapter); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + private void checkSyncedWallet() { String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE); // Just check if the current user has a synced wallet, no need to do anything else here diff --git a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java index c21bbf20..c36cbf3c 100644 --- a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java @@ -193,6 +193,10 @@ public class ClaimListAdapter extends RecyclerView.Adapter { + + private static final String RULE_CREATOR_SUBSCRIBER = "creator_subscriber"; + private static final String RULE_COMMENT = "comment"; + + private Context context; + private List items; + @Setter + private NotificationClickListener clickListener; + @Getter + @Setter + private int customizeMode; + + public NotificationListAdapter(List notifications, Context context) { + this.context = context; + this.items = new ArrayList<>(notifications); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + protected TextView titleView; + protected TextView bodyView; + protected TextView timeView; + protected SolidIconView iconView; + public ViewHolder(View v) { + super(v); + titleView = v.findViewById(R.id.notification_title); + bodyView = v.findViewById(R.id.notification_body); + timeView = v.findViewById(R.id.notification_time); + iconView = v.findViewById(R.id.notification_icon); + } + } + + public int getItemCount() { + return items != null ? items.size() : 0; + } + + public void insertNotification(LbryNotification notification, int index) { + if (!items.contains(notification)) { + items.add(index, notification); + } + notifyDataSetChanged(); + } + + public void addNotification(LbryNotification notification) { + if (!items.contains(notification)) { + items.add(notification); + } + notifyDataSetChanged(); + } + + public void addTags(List notifications) { + for (LbryNotification notification : notifications) { + if (!items.contains(notification)) { + items.add(notification); + } + } + notifyDataSetChanged(); + } + @Override + public NotificationListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) { + View v = LayoutInflater.from(context).inflate(R.layout.list_item_notification, root, false); + return new NotificationListAdapter.ViewHolder(v); + } + + private int getStringIdForRule(String rule) { + if (RULE_CREATOR_SUBSCRIBER.equalsIgnoreCase(rule)) { + return R.string.fa_heart; + } + if (RULE_COMMENT.equalsIgnoreCase(rule)) { + return R.string.fa_comment_alt; + } + return R.string.fa_asterisk; + } + + private int getColorForRule(String rule) { + if (RULE_CREATOR_SUBSCRIBER.equalsIgnoreCase(rule)) { + return Color.RED; + } + if (RULE_COMMENT.equalsIgnoreCase(rule)) { + return ContextCompat.getColor(context, R.color.nextLbryGreen); + } + + return ContextCompat.getColor(context, R.color.lbryGreen); + } + + @Override + public void onBindViewHolder(NotificationListAdapter.ViewHolder vh, int position) { + LbryNotification notification = items.get(position); + vh.titleView.setText(notification.getTitle()); + vh.titleView.setVisibility(!Helper.isNullOrEmpty(notification.getTitle()) ? View.VISIBLE : View.GONE); + vh.bodyView.setText(notification.getDescription()); + vh.timeView.setText(DateUtils.getRelativeTimeSpanString( + notification.getTimestamp().getTime(), + System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE)); + + + vh.iconView.setText(getStringIdForRule(notification.getRule())); + vh.iconView.setTextColor(getColorForRule(notification.getRule())); + + vh.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (clickListener != null) { + clickListener.onNotificationClicked(notification); + } + } + }); + } + + public interface NotificationClickListener { + void onNotificationClicked(LbryNotification notification); + } +} \ No newline at end of file diff --git a/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java index 67dabb82..035be3db 100644 --- a/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java +++ b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java @@ -4,7 +4,6 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.opengl.Visibility; import java.math.BigDecimal; import java.text.ParseException; @@ -13,16 +12,16 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import io.lbry.browser.exceptions.LbryUriException; import io.lbry.browser.model.Tag; import io.lbry.browser.model.UrlSuggestion; import io.lbry.browser.model.ViewHistory; +import io.lbry.browser.model.lbryinc.LbryNotification; import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.LbryUri; public class DatabaseHelper extends SQLiteOpenHelper { - public static final int DATABASE_VERSION = 2; + public static final int DATABASE_VERSION = 5; public static final String DATABASE_NAME = "LbryApp.db"; private static DatabaseHelper instance; @@ -48,7 +47,18 @@ public class DatabaseHelper extends SQLiteOpenHelper { ", thumbnail_url TEXT" + ", release_time INTEGER " + ", device TEXT" + - ", timestamp TEXT NOT NULL)" + ", timestamp TEXT NOT NULL)", + "CREATE TABLE notifications (" + + " id INTEGER PRIMARY KEY NOT NULL" + + ", remote_id INTEGER NOT NULL" + + ", title TEXT" + + ", description TEXT" + + ", thumbnail_url TEXT" + + ", target_url TEXT" + + ", rule TEXT" + + ", is_read INTEGER DEFAULT 0 NOT NULL" + + ", is_seen INTEGER DEFAULT 0 NOT NULL " + + ", timestamp TEXT NOT NULL)", }; private static final String[] SQL_CREATE_INDEXES = { "CREATE UNIQUE INDEX idx_subscription_url ON subscriptions (url)", @@ -56,13 +66,36 @@ public class DatabaseHelper extends SQLiteOpenHelper { "CREATE UNIQUE INDEX idx_url_history_url ON url_history (url)", "CREATE UNIQUE INDEX idx_tag_name ON tags (name)", "CREATE UNIQUE INDEX idx_view_history_url_device ON view_history (url, device)", - "CREATE INDEX idx_view_history_device ON view_history (device)" + "CREATE INDEX idx_view_history_device ON view_history (device)", + "CREATE UNIQUE INDEX idx_notification_remote_id ON notifications (remote_id)", + "CREATE INDEX idx_notification_timestamp ON notifications (timestamp)" }; private static final String[] SQL_V1_V2_UPGRADE = { "ALTER TABLE view_history ADD COLUMN currency TEXT" }; + private static final String[] SQL_V2_V3_UPGRADE = { + "CREATE TABLE notifications (" + + " id INTEGER PRIMARY KEY NOT NULL" + + ", title TEXT" + + ", description TEXT" + + ", thumbnail_url TEXT" + + ", target_url TEXT" + + ", is_read INTEGER DEFAULT 0 NOT NULL" + + ", timestamp TEXT NOT NULL)", + "CREATE INDEX idx_notification_timestamp ON notifications (timestamp)" + }; + + private static final String[] SQL_V3_V4_UPGRADE = { + "ALTER TABLE notifications ADD COLUMN remote_id INTEGER", + "CREATE UNIQUE INDEX idx_notification_remote_id ON notifications (remote_id)" + }; + private static final String[] SQL_V4_V5_UPGRADE = { + "ALTER TABLE notifications ADD COLUMN rule TEXT", + "ALTER TABLE notifications ADD COLUMN is_seen TEXT" + }; + private static final String SQL_INSERT_SUBSCRIPTION = "REPLACE INTO subscriptions (channel_name, url) VALUES (?, ?)"; private static final String SQL_CLEAR_SUBSCRIPTIONS = "DELETE FROM subscriptions"; private static final String SQL_DELETE_SUBSCRIPTION = "DELETE FROM subscriptions WHERE url = ?"; @@ -73,6 +106,11 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String SQL_CLEAR_URL_HISTORY_BEFORE_TIME = "DELETE FROM url_history WHERE timestamp < ?"; private static final String SQL_GET_RECENT_URL_HISTORY = "SELECT value, url, type FROM url_history ORDER BY timestamp DESC LIMIT 10"; + private static final String SQL_INSERT_NOTIFICATION = "REPLACE INTO notifications (remote_id, title, description, rule, target_url, is_read, is_seen, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + private static final String SQL_GET_NOTIFICATIONS = "SELECT id, title, description, rule, target_url, is_read, is_seen, timestamp FROM notifications ORDER BY timestamp DESC LIMIT 500"; + private static final String SQL_GET_UNREAD_NOTIFICATIONS_COUNT = "SELECT COUNT(id) FROM notifications WHERE is_read <> 1"; + private static final String SQL_MARK_NOTIFICATIONS_READ = "UPDATE notifications SET is_read = 1 WHERE is_read = 0"; + private static final String SQL_INSERT_VIEW_HISTORY = "REPLACE INTO view_history (url, claim_id, claim_name, cost, currency, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; private static final String SQL_GET_VIEW_HISTORY = @@ -111,6 +149,21 @@ public class DatabaseHelper extends SQLiteOpenHelper { db.execSQL(sql); } } + if (oldVersion < 3) { + for (String sql : SQL_V2_V3_UPGRADE) { + db.execSQL(sql); + } + } + if (oldVersion < 4) { + for (String sql : SQL_V3_V4_UPGRADE) { + db.execSQL(sql); + } + } + if (oldVersion < 5) { + for (String sql : SQL_V4_V5_UPGRADE) { + db.execSQL(sql); + } + } } public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { @@ -251,4 +304,60 @@ public class DatabaseHelper extends SQLiteOpenHelper { return subscriptions; } + public static void createOrUpdateNotification(LbryNotification notification, SQLiteDatabase db) { + db.execSQL(SQL_INSERT_NOTIFICATION, new Object[] { + notification.getRemoteId(), + notification.getTitle(), + notification.getDescription(), + notification.getRule(), + notification.getTargetUrl(), + notification.isRead() ? 1 : 0, + notification.isSeen() ? 1 : 0, + new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(notification.getTimestamp() != null ? notification.getTimestamp() : new Date()) + }); + } + public static List getNotifications(SQLiteDatabase db) { + List notifications = new ArrayList<>(); + Cursor cursor = null; + try { + cursor = db.rawQuery(SQL_GET_NOTIFICATIONS, null); + while (cursor.moveToNext()) { + LbryNotification notification = new LbryNotification(); + int columnIndex = 0; + notification.setId(cursor.getLong(columnIndex++)); + notification.setTitle(cursor.getString(columnIndex++)); + notification.setDescription(cursor.getString(columnIndex++)); + notification.setRule(cursor.getString(columnIndex++)); + notification.setTargetUrl(cursor.getString(columnIndex++)); + notification.setRead(cursor.getInt(columnIndex++) == 1); + notification.setSeen(cursor.getInt(columnIndex++) == 1); + try { + notification.setTimestamp(new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).parse(cursor.getString(columnIndex++))); + } catch (ParseException ex) { + // invalid timestamp (which shouldn't happen). Skip this item + continue; + } + notifications.add(notification); + } + } finally { + Helper.closeCursor(cursor); + } + return notifications; + } + public static int getUnreadNotificationsCount(SQLiteDatabase db) { + int count = 0; + Cursor cursor = null; + try { + cursor = db.rawQuery(SQL_GET_UNREAD_NOTIFICATIONS_COUNT, null); + if (cursor.moveToNext()) { + count = cursor.getInt(0); + } + } finally { + Helper.closeCursor(cursor); + } + return count; + } + public static void markNotificationsRead(SQLiteDatabase db) { + db.execSQL(SQL_MARK_NOTIFICATIONS_READ); + } } diff --git a/app/src/main/java/io/lbry/browser/model/Claim.java b/app/src/main/java/io/lbry/browser/model/Claim.java index 640b30a5..22794d8f 100644 --- a/app/src/main/java/io/lbry/browser/model/Claim.java +++ b/app/src/main/java/io/lbry/browser/model/Claim.java @@ -54,6 +54,7 @@ public class Claim { @EqualsAndHashCode.Include private boolean placeholder; private boolean placeholderAnonymous; + private boolean loadingPlaceholder; private boolean featured; private boolean unresolved; // used for featured private String address; diff --git a/app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java b/app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java new file mode 100644 index 00000000..223acbb0 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/model/lbryinc/LbryNotification.java @@ -0,0 +1,19 @@ +package io.lbry.browser.model.lbryinc; + +import java.util.Date; + +import lombok.Data; + +@Data +public class LbryNotification { + private long id; + private long remoteId; + private String title; + private String description; + private String rule; + private String thumbnailUrl; + private String targetUrl; + private boolean read; + private boolean seen; + private Date timestamp; +} diff --git a/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java b/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java index 61d49c4b..170fc4c4 100644 --- a/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java @@ -45,10 +45,12 @@ public class CommentListTask extends AsyncTask> { options.put("claim_id", claim); options.put("page", page); options.put("page_size", pageSize); + options.put("hidden", false); options.put("include_replies", false); options.put("is_channel_signature_valid", true); + options.put("skip_validation", true); options.put("visible", true); - options.put("hidden", false); + JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_COMMENT_LIST, options); JSONArray items = result.getJSONArray("items"); diff --git a/app/src/main/java/io/lbry/browser/tasks/lbryinc/NotificationListTask.java b/app/src/main/java/io/lbry/browser/tasks/lbryinc/NotificationListTask.java new file mode 100644 index 00000000..dd16b146 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/lbryinc/NotificationListTask.java @@ -0,0 +1,120 @@ +package io.lbry.browser.tasks.lbryinc; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.os.AsyncTask; +import android.view.View; +import android.widget.ProgressBar; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import io.lbry.browser.MainActivity; +import io.lbry.browser.data.DatabaseHelper; +import io.lbry.browser.exceptions.LbryioRequestException; +import io.lbry.browser.exceptions.LbryioResponseException; +import io.lbry.browser.model.lbryinc.LbryNotification; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbryio; + +public class NotificationListTask extends AsyncTask> { + private Context context; + private ListNotificationsHandler handler; + private ProgressBar progressBar; + private Exception error; + + public NotificationListTask(Context context, ProgressBar progressBar, ListNotificationsHandler handler) { + this.context = context; + this.progressBar = progressBar; + this.handler = handler; + } + protected void onPreExecute() { + Helper.setViewVisibility(progressBar, View.VISIBLE); + } + protected List doInBackground(Void... params) { + List notifications = new ArrayList<>(); + SQLiteDatabase db = null; + try { + JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("notification", "list", context)); + if (array != null) { + for (int i = 0; i < array.length(); i++) { + JSONObject item = array.getJSONObject(i); + if (item.has("notification_parameters")) { + LbryNotification notification = new LbryNotification(); + + JSONObject notificationParams = item.getJSONObject("notification_parameters"); + if (notificationParams.has("device")) { + JSONObject device = notificationParams.getJSONObject("device"); + notification.setTitle(Helper.getJSONString("title", null, device)); + notification.setDescription(Helper.getJSONString("text", null, device)); + notification.setTargetUrl(Helper.getJSONString("target", null, device)); + } + if (notificationParams.has("dynamic") && !notificationParams.isNull("dynamic")) { + JSONObject dynamic = notificationParams.getJSONObject("dynamic"); + if (dynamic.has("channelURI")) { + String channelUrl = Helper.getJSONString("channelURI", null, dynamic); + if (!Helper.isNullOrEmpty(channelUrl)) { + notification.setTargetUrl(channelUrl); + } + } + if (dynamic.has("hash") && "comment".equalsIgnoreCase(Helper.getJSONString("notification_rule", null, item))) { + notification.setTargetUrl(String.format("%s?comment_hash=%s", notification.getTargetUrl(), dynamic.getString("hash"))); + } + } + + notification.setRule(Helper.getJSONString("notification_rule", null, item)); + notification.setRemoteId(Helper.getJSONLong("id", 0, item)); + notification.setRead(Helper.getJSONBoolean("is_read", false, item)); + notification.setSeen(Helper.getJSONBoolean("is_seen", false, item)); + + try { + SimpleDateFormat dateFormat = new SimpleDateFormat(Helper.ISO_DATE_FORMAT_JSON, Locale.US); + notification.setTimestamp(dateFormat.parse(Helper.getJSONString("created_at", dateFormat.format(new Date()), item))); + } catch (ParseException ex) { + notification.setTimestamp(new Date()); + } + + if (notification.getRemoteId() > 0 && !Helper.isNullOrEmpty(notification.getDescription())) { + notifications.add(notification); + } + } + } + + if (context instanceof MainActivity) { + db = ((MainActivity) context).getDbHelper().getWritableDatabase(); + for (LbryNotification notification : notifications) { + DatabaseHelper.createOrUpdateNotification(notification, db); + } + } + } + } catch (ClassCastException | LbryioRequestException | LbryioResponseException | JSONException | IllegalStateException ex) { + error = ex; + return null; + } + + return notifications; + } + protected void onPostExecute(List notifications) { + Helper.setViewVisibility(progressBar, View.GONE); + if (handler != null) { + if (notifications != null) { + handler.onSuccess(notifications); + } else { + handler.onError(error); + } + } + } + + public interface ListNotificationsHandler { + void onSuccess(List notifications); + void onError(Exception exception); + } +} diff --git a/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java b/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java index 9adec2d7..6a34eca8 100644 --- a/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java @@ -38,6 +38,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatSpinner; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; +import androidx.core.widget.NestedScrollView; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -233,6 +234,9 @@ public class FileViewFragment extends BaseFragment implements private View inlineChannelCreatorProgress; private MaterialButton inlineChannelCreatorCreateButton; + // if this is set, scroll to the specific comment on load + private String commentHash; + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_file_view, container, false); @@ -357,9 +361,27 @@ public class FileViewFragment extends BaseFragment implements } } if (params.containsKey("url")) { - newUrl = params.get("url").toString(); - if (claim == null || !newUrl.equalsIgnoreCase(currentUrl)) { - updateRequired = true; + LbryUri newLbryUri = LbryUri.tryParse(params.get("url").toString()); + if (newLbryUri != null) { + newUrl = newLbryUri.toString(); + String qs = newLbryUri.getQueryString(); + if (!Helper.isNullOrEmpty(qs)) { + String[] qsPairs = qs.split("&"); + for (String pair : qsPairs) { + String[] parts = pair.split("="); + if (parts.length < 2) { + continue; + } + if ("comment_hash".equalsIgnoreCase(parts[0])) { + commentHash = parts[1]; + break; + } + } + } + + if (claim == null || !newUrl.equalsIgnoreCase(currentUrl)) { + updateRequired = true; + } } } } else if (currentUrl != null) { @@ -567,7 +589,6 @@ public class FileViewFragment extends BaseFragment implements } checkOwnClaim(); fetchChannels(); - checkAndLoadComments(); } private String getStreamingUrl() { @@ -709,7 +730,6 @@ public class FileViewFragment extends BaseFragment implements } else { onSdkReady(); } - checkCommentSdkInitializing(); } public void onStop() { @@ -1513,7 +1533,6 @@ public class FileViewFragment extends BaseFragment implements private void checkAndLoadComments() { View root = getView(); if (root != null) { - checkCommentSdkInitializing(); RecyclerView commentsList = root.findViewById(R.id.file_view_comments_list); if (commentsList == null || commentsList.getAdapter() == null || commentsList.getAdapter().getItemCount() == 0) { loadComments(); @@ -1521,14 +1540,6 @@ public class FileViewFragment extends BaseFragment implements } } - private void checkCommentSdkInitializing() { - View root = getView(); - if (root != null) { - TextView commentsSDKInitializing = root.findViewById(R.id.file_view_comments_sdk_initializing); - Helper.setViewVisibility(commentsSDKInitializing, Lbry.SDK_READY ? View.GONE : View.VISIBLE); - } - } - private void showUnsupportedView() { View root = getView(); if (root != null) { @@ -2075,10 +2086,21 @@ public class FileViewFragment extends BaseFragment implements // reset the list view View root = getView(); if (claim != null && root != null) { + Context context = getContext(); + + List loadingPlaceholders = new ArrayList<>(); + for (int i = 0; i < 15; i++) { + Claim placeholder = new Claim(); + placeholder.setLoadingPlaceholder(true); + loadingPlaceholders.add(placeholder); + } + relatedContentAdapter = new ClaimListAdapter(loadingPlaceholders, context); + RecyclerView relatedContentList = root.findViewById(R.id.file_view_related_content_list); + relatedContentList.setAdapter(relatedContentAdapter); + String title = claim.getTitle(); String claimId = claim.getClaimId(); ProgressBar relatedLoading = root.findViewById(R.id.file_view_related_content_progress); - Context context = getContext(); boolean canShowMatureContent = false; if (context != null) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); @@ -2098,10 +2120,14 @@ public class FileViewFragment extends BaseFragment implements Context ctx = getContext(); if (ctx != null) { - relatedContentAdapter = new ClaimListAdapter(filteredClaims, ctx); + relatedContentAdapter.setItems(filteredClaims); relatedContentAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() { @Override public void onClaimClicked(Claim claim) { + if (claim.isLoadingPlaceholder()) { + return; + } + if (context instanceof MainActivity) { MainActivity activity = (MainActivity) context; if (claim.getName().startsWith("@")) { @@ -2123,6 +2149,10 @@ public class FileViewFragment extends BaseFragment implements v.findViewById(R.id.file_view_no_related_content), relatedContentAdapter == null || relatedContentAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); } + + // if related content loads before comment, this will affect the scroll position + // so just ensure that we are at the correct position + scrollToCommentHash(); } } @@ -2137,9 +2167,9 @@ public class FileViewFragment extends BaseFragment implements private void loadComments() { View root = getView(); - ProgressBar relatedLoading = root.findViewById(R.id.file_view_comments_progress); + ProgressBar commentsLoading = root.findViewById(R.id.file_view_comments_progress); if (claim != null && root != null) { - CommentListTask task = new CommentListTask(1, 500, claim.getClaimId(), relatedLoading, new CommentListHandler() { + CommentListTask task = new CommentListTask(1, 200, claim.getClaimId(), commentsLoading, new CommentListHandler() { @Override public void onSuccess(List comments, boolean hasReachedEnd) { Context ctx = getContext(); @@ -2167,6 +2197,7 @@ public class FileViewFragment extends BaseFragment implements relatedContentList.setAdapter(commentListAdapter); commentListAdapter.notifyDataSetChanged(); + scrollToCommentHash(); checkNoComments(); resolveCommentPosters(); } @@ -2181,6 +2212,20 @@ public class FileViewFragment extends BaseFragment implements } } + private void scrollToCommentHash() { + View root = getView(); + // check for the position of commentHash if set + if (root != null && !Helper.isNullOrEmpty(commentHash) && commentListAdapter != null && commentListAdapter.getItemCount() > 0) { + RecyclerView commentList = root.findViewById(R.id.file_view_comments_list); + int position = commentListAdapter.getPositionForComment(commentHash); + if (position > -1) { + NestedScrollView scrollView = root.findViewById(R.id.file_view_scroll_view); + scrollView.requestChildFocus(commentList, commentList); + commentList.getLayoutManager().scrollToPosition(position); + } + } + } + private void checkNoComments() { View root = getView(); if (root != null) { @@ -2194,7 +2239,7 @@ public class FileViewFragment extends BaseFragment implements long st = System.currentTimeMillis();; List urlsToResolve = new ArrayList<>(commentListAdapter.getClaimUrlsToResolve()); if (urlsToResolve.size() > 0) { - ResolveTask task = new ResolveTask(urlsToResolve, Lbry.SDK_CONNECTION_STRING, null, new ClaimListResultHandler() { + ResolveTask task = new ResolveTask(urlsToResolve, Lbry.LBRY_TV_CONNECTION_STRING, null, new ClaimListResultHandler() { @Override public void onSuccess(List claims) { if (commentListAdapter != null) { diff --git a/app/src/main/java/io/lbry/browser/utils/Helper.java b/app/src/main/java/io/lbry/browser/utils/Helper.java index eac70533..5114e840 100644 --- a/app/src/main/java/io/lbry/browser/utils/Helper.java +++ b/app/src/main/java/io/lbry/browser/utils/Helper.java @@ -70,6 +70,7 @@ public final class Helper { public static final String METHOD_GET = "GET"; public static final String METHOD_POST = "POST"; public static final String ISO_DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + public static final String ISO_DATE_FORMAT_JSON = "yyyy-MM-dd'T'HH:mm:ss'Z'"; public static final String SDK_AMOUNT_FORMAT = "0.0#######"; public static final MediaType FORM_MEDIA_TYPE = MediaType.parse("application/x-www-form-urlencoded"); public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8"); diff --git a/app/src/main/res/drawable-anydpi/ic_notifications.xml b/app/src/main/res/drawable-anydpi/ic_notifications.xml new file mode 100644 index 00000000..0dbf589c --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_notifications.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/ic_notifications.png b/app/src/main/res/drawable-hdpi/ic_notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..3a83a649ffda7df0a1d77ada089cd0dd350248e7 GIT binary patch literal 273 zcmV+s0q*{ZP)SOk$PA_{H~?uzrt*&w*_MMqSn4KyDuPS zTVI%(5)L5#W{Jj3sJjDZ=*BaUpw$S>P>lqg0iY8SbOnGeNKg*|^+=EafCLgG5oJ&Y zWzb)MXwV)7;#o0a#-Ti6+)*AeZs$qwkhhY#LR+svAwi7=bO;HWF@SpN_<5Rtza+Ij XSc4UXr(rY200000NkvXXu0mjfOh#>@ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_notifications.png b/app/src/main/res/drawable-mdpi/ic_notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a94d02822b2025d3b8b8769ac36aba68d19510 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjlRRA}1CrB7R`2YXEk3$aY zQJ*av7*akYX>41x?co}SIbA7?9cR6C55H}wu$uJHfJbQa;wuIEI&6!R^^16?Gd*H; zi7=4wXg#q}p^xKmqQxW`?p+=m7IeyRKbx3vkf%r|!iP(4YeGjP!xaISRjdlOTTJFu zcxxEU*|TFodas$_$)&=XiH&js{!K}HCti+VU~q}wvfq8u=iNZpF?hQAxvXxVFl6%d?V zxk`&=+kvge^x7ve87;_};K^m6%jg#5aI=l&BTGan>y_7&6ubNzgZnFlcfU>1_~iZI zYxADRoH{$6Ua#+{j}43Yr1`;(W5e$mfv5W$t(*2dX6w;l{?_rSPj+ezi;WG-jaoMm z#$x_YeN&VkyfA#=AXxBGqTpFoz~MsyEFyBuT-(%rbxa(Xp=g17-DB$ma?YFf%;!j| zyu+bZxrf88a?b3inufju_r2S`&J|Zwc+Yu4mF0-+ec!j=&qtg}6}g^%=Y38cV+3WkZ$c%0*mYx`$8j9TaU56fk|gO$ep;6zA22OxEXBF>XDtFH$>8r-1_blf zJy0ABBygN+ku1Tf2FWrUvq+ZWm_xD{$IQn+fe6HLoCA}4uvp`EEL;S-TLb9UMWA7A zzW-bjkp5%cV*#9deXOf@sPhZbgS6}VvDSCeiQbv?aBI`_Pq6869LI4S_s6{hYDz literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/bg_notification_badge.xml b/app/src/main/res/drawable/bg_notification_badge.xml new file mode 100644 index 00000000..3af51c12 --- /dev/null +++ b/app/src/main/res/drawable/bg_notification_badge.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml index dc408393..6777cd17 100644 --- a/app/src/main/res/layout/app_bar_main.xml +++ b/app/src/main/res/layout/app_bar_main.xml @@ -28,20 +28,22 @@ android:focusable="true" android:focusableInTouchMode="true" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginRight="24dp"> + android:layout_height="wrap_content"> @@ -61,6 +64,39 @@ android:src="@drawable/ic_close" android:tint="@color/actionBarForeground" /> + + + + + diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 6bd21c94..a981e4d7 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -33,6 +33,53 @@ android:layout_height="match_parent" /> + + + + + + + + + - - + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_stream.xml b/app/src/main/res/layout/list_item_stream.xml index 04f785a8..6e73ec64 100644 --- a/app/src/main/res/layout/list_item_stream.xml +++ b/app/src/main/res/layout/list_item_stream.xml @@ -138,6 +138,13 @@ android:src="@drawable/ic_check" android:tint="@color/nextLbryGreen" /> + + + + The lbrynet.log file could not be found. The lbrynet.log file cannot be shared due to permission restrictions. + + It\'s quiet here! New notifications will be displayed when you receive them. + @@ -626,4 +629,7 @@ + + + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index c54f18e5..8e9647bf 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -7,6 +7,7 @@ app:iconSpaceReserved="false"> +