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.
This commit is contained in:
Akinwale Ariwodola 2020-08-18 14:19:35 +01:00 committed by GitHub
parent ddcb190457
commit 4d024c06cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 892 additions and 57 deletions

View file

@ -101,8 +101,8 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
__32bitImplementation 'io.lbry:lbrysdk32:0.79.1' __32bitImplementation 'io.lbry:lbrysdk32:0.80.0'
__64bitImplementation 'io.lbry:lbrysdk64:0.79.1' __64bitImplementation 'io.lbry:lbrysdk64:0.80.0'
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

View file

@ -6,6 +6,7 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; 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.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage; 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.browser.utils.LbryAnalytics;
import io.lbry.lbrysdk.LbrynetService;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public class LbrynetMessagingService extends FirebaseMessagingService { 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 TAG = "LbrynetMessagingService";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL"; 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_SUBSCRIPTION = "subscription";
private static final String TYPE_REWARD = "reward"; private static final String TYPE_REWARD = "reward";
private static final String TYPE_INTERESTS = "interests"; private static final String TYPE_INTERESTS = "interests";
@ -52,10 +56,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
String title = payload.get("title"); String title = payload.get("title");
String body = payload.get("body"); String body = payload.get("body");
String name = payload.get("name"); // notification name String name = payload.get("name"); // notification name
String contentTitle = payload.get("content_title"); String hash = payload.get("hash"); // comment hash
String channelUrl = payload.get("channel_url");
//String publishTime = payload.get("publish_time");
String publishTime = null;
if (type != null && getEnabledTypes().indexOf(type) > -1 && body != null && body.trim().length() > 0) { if (type != null && getEnabledTypes().indexOf(type) > -1 && body != null && body.trim().length() > 0) {
// only log the receive event for valid notifications received // 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); 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. * @param messageBody FCM message body received.
*/ */
private void sendNotification(String title, String messageBody, String type, String url, String name, private void sendNotification(String title, String messageBody, String type, String url, String name) {
String contentTitle, String channelUrl, String publishTime) {
//Intent intent = new Intent(this, MainActivity.class); //Intent intent = new Intent(this, MainActivity.class);
//intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); //intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (url == null) { if (url == null) {
@ -143,6 +170,9 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
List<String> enabledTypes = new ArrayList<String>(); List<String> enabledTypes = new ArrayList<String>();
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_COMMENTS, true)) {
enabledTypes.add(TYPE_COMMENT);
}
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS, true)) { if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS, true)) {
enabledTypes.add(TYPE_SUBSCRIPTION); enabledTypes.add(TYPE_SUBSCRIPTION);
} }

View file

@ -115,6 +115,7 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import io.lbry.browser.adapter.NavigationMenuAdapter; import io.lbry.browser.adapter.NavigationMenuAdapter;
import io.lbry.browser.adapter.NotificationListAdapter;
import io.lbry.browser.adapter.UrlSuggestionListAdapter; import io.lbry.browser.adapter.UrlSuggestionListAdapter;
import io.lbry.browser.data.DatabaseHelper; import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.dialog.ContentScopeDialogFragment; 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.UrlSuggestion;
import io.lbry.browser.model.WalletBalance; import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.model.WalletSync; 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.Reward;
import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.tasks.GenericTaskHandler; 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.LighthouseAutoCompleteTask;
import io.lbry.browser.tasks.MergeSubscriptionsTask; import io.lbry.browser.tasks.MergeSubscriptionsTask;
import io.lbry.browser.tasks.claim.ResolveTask; 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.localdata.FetchRecentUrlHistoryTask;
import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler; import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler;
import io.lbry.browser.tasks.wallet.LoadSharedUserStateTask; import io.lbry.browser.tasks.wallet.LoadSharedUserStateTask;
@ -224,6 +227,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
@Getter @Getter
private String firebaseMessagingToken; private String firebaseMessagingToken;
private NotificationListAdapter notificationListAdapter;
private Map<String, Fragment> openNavFragments; private Map<String, Fragment> openNavFragments;
private static final Map<Class, Integer> fragmentClassNavIdMap = new HashMap<>(); private static final Map<Class, Integer> fragmentClassNavIdMap = new HashMap<>();
static { 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_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_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_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_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_REWARDS = "io.lbry.browser.preference.notifications.Rewards";
public static final String PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS = "io.lbry.browser.preference.notifications.ContentInterests"; 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 // setup uri bar
setupUriBar(); setupUriBar();
initNotificationsPage();
loadUnreadNotificationsCount();
// other // other
pendingSyncSetQueue = new ArrayList<>(); pendingSyncSetQueue = new ArrayList<>();
@ -475,6 +483,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public void onDrawerSlide(View drawerView, float slideOffset) { public void onDrawerSlide(View drawerView, float slideOffset) {
if (slideOffset != 0) { if (slideOffset != 0) {
clearWunderbarFocus(findViewById(R.id.wunderbar)); clearWunderbarFocus(findViewById(R.id.wunderbar));
hideNotifications();
} }
super.onDrawerSlide(drawerView, slideOffset); 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() { findViewById(R.id.global_now_playing_card).setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) { if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) {
hideNotifications();
openFileUrl(nowPlayingClaimUrl); openFileUrl(nowPlayingClaimUrl);
} }
} }
@ -551,7 +573,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public boolean isBackgroundPlaybackEnabled() { public boolean isBackgroundPlaybackEnabled() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
return sp.getBoolean(PREFERENCE_KEY_BACKGROUND_PLAYBACK, false); return sp.getBoolean(PREFERENCE_KEY_BACKGROUND_PLAYBACK, true);
} }
public boolean initialSubscriptionMergeDone() { public boolean initialSubscriptionMergeDone() {
@ -717,8 +739,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
} }
private void openSelectedMenuItem() { private void openSelectedMenuItem() {
hideNotifications();
switch (selectedMenuItemId) { switch (selectedMenuItemId) {
// TODO: reverse map lookup for class?
case NavMenuItem.ID_ITEM_FOLLOWING: case NavMenuItem.ID_ITEM_FOLLOWING:
openFragment(FollowingFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING); openFragment(FollowingFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING);
break; break;
@ -1067,7 +1090,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
View container = findViewById(R.id.url_suggestions_container); View container = findViewById(R.id.url_suggestions_container);
View closeIcon = findViewById(R.id.wunderbar_close); View closeIcon = findViewById(R.id.wunderbar_close);
EditText wunderbar = findViewById(R.id.wunderbar); 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); container.setVisibility(visible ? View.VISIBLE : View.GONE);
closeIcon.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 @Override
public void onFocusChange(View view, boolean hasFocus) { public void onFocusChange(View view, boolean hasFocus) {
if (hasFocus) { if (hasFocus) {
hideNotifications();
findViewById(R.id.wunderbar_notifications).setVisibility(View.INVISIBLE);
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(view, 0); imm.showSoftInput(view, 0);
} else {
findViewById(R.id.wunderbar_notifications).setVisibility(View.VISIBLE);
} }
if (canShowUrlSuggestions()) { 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 String CHANNEL_ID_PLAYBACK = "io.lbry.browser.LBRY_PLAYBACK_CHANNEL";
private static final int PLAYBACK_NOTIFICATION_ID = 3; 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() { public void initPlaybackNotification() {
if (isBackgroundPlaybackEnabled()) { if (isBackgroundPlaybackEnabled()) {
playerNotificationManager.setPlayer(MainActivity.appPlayer); playerNotificationManager.setPlayer(MainActivity.appPlayer);
@ -2006,6 +2042,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
intentFilter.addAction(ACTION_OPEN_REWARDS_PAGE); intentFilter.addAction(ACTION_OPEN_REWARDS_PAGE);
intentFilter.addAction(ACTION_PUBLISH_SUCCESSFUL); intentFilter.addAction(ACTION_PUBLISH_SUCCESSFUL);
intentFilter.addAction(ACTION_SAVE_SHARED_USER_STATE); intentFilter.addAction(ACTION_SAVE_SHARED_USER_STATE);
intentFilter.addAction(LbrynetMessagingService.ACTION_NOTIFICATION_RECEIVED);
requestsReceiver = new BroadcastReceiver() { requestsReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@ -2026,6 +2063,25 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
saveSharedUserState(); saveSharedUserState();
} else if (ACTION_PUBLISH_SUCCESSFUL.equalsIgnoreCase(action)) { } else if (ACTION_PUBLISH_SUCCESSFUL.equalsIgnoreCase(action)) {
openPublishesOnSuccessfulPublish(); 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(); 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<Void, Void, Void>() {
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 @Override
public void onBackPressed() { public void onBackPressed() {
if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) { if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) {
clearWunderbarFocus(findViewById(R.id.wunderbar)); clearWunderbarFocus(findViewById(R.id.wunderbar));
return; return;
} }
if (findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) {
hideNotifications();
return;
}
if (backPressInterceptor != null && backPressInterceptor.onBackPressed()) { if (backPressInterceptor != null && backPressInterceptor.onBackPressed()) {
return; return;
} }
@ -3051,6 +3142,90 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 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<Void, Void, Integer>() {
@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<LbryNotification> 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<Void, Void, List<LbryNotification>>() {
protected void onPreExecute() {
findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE);
findViewById(R.id.notifications_progress).setVisibility(View.VISIBLE);
}
@Override
protected List<LbryNotification> doInBackground(Void... params) {
List<LbryNotification> notifications = new ArrayList<>();
try {
SQLiteDatabase db = dbHelper.getReadableDatabase();
notifications = DatabaseHelper.getNotifications(db);
} catch (Exception ex) {
// pass
}
return notifications;
}
protected void onPostExecute(List<LbryNotification> 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() { private void checkSyncedWallet() {
String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE); 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 // Just check if the current user has a synced wallet, no need to do anything else here

View file

@ -193,6 +193,10 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
protected TextView fileSizeView; protected TextView fileSizeView;
protected ProgressBar downloadProgressView; protected ProgressBar downloadProgressView;
protected TextView deviceView; protected TextView deviceView;
protected View loadingImagePlaceholder;
protected View loadingTextPlaceholder1;
protected View loadingTextPlaceholder2;
public ViewHolder(View v) { public ViewHolder(View v) {
super(v); super(v);
feeContainer = v.findViewById(R.id.claim_fee_container); feeContainer = v.findViewById(R.id.claim_fee_container);
@ -212,6 +216,10 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
fileSizeView = v.findViewById(R.id.claim_file_size); fileSizeView = v.findViewById(R.id.claim_file_size);
downloadProgressView = v.findViewById(R.id.claim_download_progress); downloadProgressView = v.findViewById(R.id.claim_download_progress);
deviceView = v.findViewById(R.id.claim_view_device); deviceView = v.findViewById(R.id.claim_view_device);
loadingImagePlaceholder = v.findViewById(R.id.claim_thumbnail_placeholder);
loadingTextPlaceholder1 = v.findViewById(R.id.claim_text_loading_placeholder_1);
loadingTextPlaceholder2 = v.findViewById(R.id.claim_text_loading_placeholder_2);
} }
} }
@ -394,7 +402,7 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
}); });
vh.publishTimeView.setVisibility(!isPending ? View.VISIBLE : View.GONE); vh.publishTimeView.setVisibility(!isPending ? View.VISIBLE : View.GONE);
vh.pendingTextView.setVisibility(isPending ? View.VISIBLE : View.GONE); vh.pendingTextView.setVisibility(isPending && !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
vh.repostInfoView.setVisibility(isRepost && type != VIEW_TYPE_FEATURED ? View.VISIBLE : View.GONE); vh.repostInfoView.setVisibility(isRepost && type != VIEW_TYPE_FEATURED ? View.VISIBLE : View.GONE);
vh.repostChannelView.setText(isRepost ? original.getSigningChannel().getName() : null); vh.repostChannelView.setText(isRepost ? original.getSigningChannel().getName() : null);
vh.repostChannelView.setOnClickListener(new View.OnClickListener() { vh.repostChannelView.setOnClickListener(new View.OnClickListener() {
@ -417,6 +425,13 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE); vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE);
Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context); Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context);
Helper.setViewVisibility(vh.loadingImagePlaceholder, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.loadingTextPlaceholder1, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.loadingTextPlaceholder2, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.titleView, !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.publisherView, !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.publishTimeView, !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
if (type == VIEW_TYPE_FEATURED && item.isUnresolved()) { if (type == VIEW_TYPE_FEATURED && item.isUnresolved()) {
vh.durationView.setVisibility(View.GONE); vh.durationView.setVisibility(View.GONE);
vh.titleView.setText("Nothing here. Publish something!"); vh.titleView.setText("Nothing here. Publish something!");

View file

@ -61,6 +61,16 @@ public class CommentListAdapter extends RecyclerView.Adapter<CommentListAdapter.
notifyDataSetChanged(); notifyDataSetChanged();
} }
public int getPositionForComment(String commentHash) {
for (int i = 0; i < items.size(); i++) {
if (commentHash.equalsIgnoreCase(items.get(i).getId())) {
return i;
}
}
return -1;
}
@Override @Override
public int getItemCount() { public int getItemCount() {
return items != null ? items.size() : 0; return items != null ? items.size() : 0;

View file

@ -0,0 +1,136 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.graphics.Color;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.ui.controls.SolidIconView;
import io.lbry.browser.utils.Helper;
import lombok.Getter;
import lombok.Setter;
public class NotificationListAdapter extends RecyclerView.Adapter<NotificationListAdapter.ViewHolder> {
private static final String RULE_CREATOR_SUBSCRIBER = "creator_subscriber";
private static final String RULE_COMMENT = "comment";
private Context context;
private List<LbryNotification> items;
@Setter
private NotificationClickListener clickListener;
@Getter
@Setter
private int customizeMode;
public NotificationListAdapter(List<LbryNotification> 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<LbryNotification> 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);
}
}

View file

@ -4,7 +4,6 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.opengl.Visibility;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.ParseException; import java.text.ParseException;
@ -13,16 +12,16 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.model.Tag; import io.lbry.browser.model.Tag;
import io.lbry.browser.model.UrlSuggestion; import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.model.ViewHistory; import io.lbry.browser.model.ViewHistory;
import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri; import io.lbry.browser.utils.LbryUri;
public class DatabaseHelper extends SQLiteOpenHelper { 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"; public static final String DATABASE_NAME = "LbryApp.db";
private static DatabaseHelper instance; private static DatabaseHelper instance;
@ -48,7 +47,18 @@ public class DatabaseHelper extends SQLiteOpenHelper {
", thumbnail_url TEXT" + ", thumbnail_url TEXT" +
", release_time INTEGER " + ", release_time INTEGER " +
", device TEXT" + ", 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 = { private static final String[] SQL_CREATE_INDEXES = {
"CREATE UNIQUE INDEX idx_subscription_url ON subscriptions (url)", "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_url_history_url ON url_history (url)",
"CREATE UNIQUE INDEX idx_tag_name ON tags (name)", "CREATE UNIQUE INDEX idx_tag_name ON tags (name)",
"CREATE UNIQUE INDEX idx_view_history_url_device ON view_history (url, device)", "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 = { private static final String[] SQL_V1_V2_UPGRADE = {
"ALTER TABLE view_history ADD COLUMN currency TEXT" "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_INSERT_SUBSCRIPTION = "REPLACE INTO subscriptions (channel_name, url) VALUES (?, ?)";
private static final String SQL_CLEAR_SUBSCRIPTIONS = "DELETE FROM subscriptions"; private static final String SQL_CLEAR_SUBSCRIPTIONS = "DELETE FROM subscriptions";
private static final String SQL_DELETE_SUBSCRIPTION = "DELETE FROM subscriptions WHERE url = ?"; 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_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_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 = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; "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 = private static final String SQL_GET_VIEW_HISTORY =
@ -111,6 +149,21 @@ public class DatabaseHelper extends SQLiteOpenHelper {
db.execSQL(sql); 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) { public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
@ -251,4 +304,60 @@ public class DatabaseHelper extends SQLiteOpenHelper {
return subscriptions; 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<LbryNotification> getNotifications(SQLiteDatabase db) {
List<LbryNotification> 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);
}
} }

View file

@ -54,6 +54,7 @@ public class Claim {
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
private boolean placeholder; private boolean placeholder;
private boolean placeholderAnonymous; private boolean placeholderAnonymous;
private boolean loadingPlaceholder;
private boolean featured; private boolean featured;
private boolean unresolved; // used for featured private boolean unresolved; // used for featured
private String address; private String address;

View file

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

View file

@ -45,10 +45,12 @@ public class CommentListTask extends AsyncTask<Void, Void, List<Comment>> {
options.put("claim_id", claim); options.put("claim_id", claim);
options.put("page", page); options.put("page", page);
options.put("page_size", pageSize); options.put("page_size", pageSize);
options.put("hidden", false);
options.put("include_replies", false); options.put("include_replies", false);
options.put("is_channel_signature_valid", true); options.put("is_channel_signature_valid", true);
options.put("skip_validation", true);
options.put("visible", true); options.put("visible", true);
options.put("hidden", false);
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_COMMENT_LIST, options); JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_COMMENT_LIST, options);
JSONArray items = result.getJSONArray("items"); JSONArray items = result.getJSONArray("items");

View file

@ -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<Void, Void, List<LbryNotification>> {
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<LbryNotification> doInBackground(Void... params) {
List<LbryNotification> 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<LbryNotification> 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<LbryNotification> notifications);
void onError(Exception exception);
}
}

View file

@ -38,6 +38,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatSpinner; import androidx.appcompat.widget.AppCompatSpinner;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.widget.NestedScrollView;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -233,6 +234,9 @@ public class FileViewFragment extends BaseFragment implements
private View inlineChannelCreatorProgress; private View inlineChannelCreatorProgress;
private MaterialButton inlineChannelCreatorCreateButton; private MaterialButton inlineChannelCreatorCreateButton;
// if this is set, scroll to the specific comment on load
private String commentHash;
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_file_view, container, false); View root = inflater.inflate(R.layout.fragment_file_view, container, false);
@ -357,11 +361,29 @@ public class FileViewFragment extends BaseFragment implements
} }
} }
if (params.containsKey("url")) { if (params.containsKey("url")) {
newUrl = params.get("url").toString(); 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)) { if (claim == null || !newUrl.equalsIgnoreCase(currentUrl)) {
updateRequired = true; updateRequired = true;
} }
} }
}
} else if (currentUrl != null) { } else if (currentUrl != null) {
updateRequired = true; updateRequired = true;
} else if (context instanceof MainActivity) { } else if (context instanceof MainActivity) {
@ -567,7 +589,6 @@ public class FileViewFragment extends BaseFragment implements
} }
checkOwnClaim(); checkOwnClaim();
fetchChannels(); fetchChannels();
checkAndLoadComments();
} }
private String getStreamingUrl() { private String getStreamingUrl() {
@ -709,7 +730,6 @@ public class FileViewFragment extends BaseFragment implements
} else { } else {
onSdkReady(); onSdkReady();
} }
checkCommentSdkInitializing();
} }
public void onStop() { public void onStop() {
@ -1513,7 +1533,6 @@ public class FileViewFragment extends BaseFragment implements
private void checkAndLoadComments() { private void checkAndLoadComments() {
View root = getView(); View root = getView();
if (root != null) { if (root != null) {
checkCommentSdkInitializing();
RecyclerView commentsList = root.findViewById(R.id.file_view_comments_list); RecyclerView commentsList = root.findViewById(R.id.file_view_comments_list);
if (commentsList == null || commentsList.getAdapter() == null || commentsList.getAdapter().getItemCount() == 0) { if (commentsList == null || commentsList.getAdapter() == null || commentsList.getAdapter().getItemCount() == 0) {
loadComments(); 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() { private void showUnsupportedView() {
View root = getView(); View root = getView();
if (root != null) { if (root != null) {
@ -2075,10 +2086,21 @@ public class FileViewFragment extends BaseFragment implements
// reset the list view // reset the list view
View root = getView(); View root = getView();
if (claim != null && root != null) { if (claim != null && root != null) {
Context context = getContext();
List<Claim> 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 title = claim.getTitle();
String claimId = claim.getClaimId(); String claimId = claim.getClaimId();
ProgressBar relatedLoading = root.findViewById(R.id.file_view_related_content_progress); ProgressBar relatedLoading = root.findViewById(R.id.file_view_related_content_progress);
Context context = getContext();
boolean canShowMatureContent = false; boolean canShowMatureContent = false;
if (context != null) { if (context != null) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
@ -2098,10 +2120,14 @@ public class FileViewFragment extends BaseFragment implements
Context ctx = getContext(); Context ctx = getContext();
if (ctx != null) { if (ctx != null) {
relatedContentAdapter = new ClaimListAdapter(filteredClaims, ctx); relatedContentAdapter.setItems(filteredClaims);
relatedContentAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() { relatedContentAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() {
@Override @Override
public void onClaimClicked(Claim claim) { public void onClaimClicked(Claim claim) {
if (claim.isLoadingPlaceholder()) {
return;
}
if (context instanceof MainActivity) { if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context; MainActivity activity = (MainActivity) context;
if (claim.getName().startsWith("@")) { if (claim.getName().startsWith("@")) {
@ -2123,6 +2149,10 @@ public class FileViewFragment extends BaseFragment implements
v.findViewById(R.id.file_view_no_related_content), v.findViewById(R.id.file_view_no_related_content),
relatedContentAdapter == null || relatedContentAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); 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() { private void loadComments() {
View root = getView(); 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) { 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 @Override
public void onSuccess(List<Comment> comments, boolean hasReachedEnd) { public void onSuccess(List<Comment> comments, boolean hasReachedEnd) {
Context ctx = getContext(); Context ctx = getContext();
@ -2167,6 +2197,7 @@ public class FileViewFragment extends BaseFragment implements
relatedContentList.setAdapter(commentListAdapter); relatedContentList.setAdapter(commentListAdapter);
commentListAdapter.notifyDataSetChanged(); commentListAdapter.notifyDataSetChanged();
scrollToCommentHash();
checkNoComments(); checkNoComments();
resolveCommentPosters(); 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() { private void checkNoComments() {
View root = getView(); View root = getView();
if (root != null) { if (root != null) {
@ -2194,7 +2239,7 @@ public class FileViewFragment extends BaseFragment implements
long st = System.currentTimeMillis();; long st = System.currentTimeMillis();;
List<String> urlsToResolve = new ArrayList<>(commentListAdapter.getClaimUrlsToResolve()); List<String> urlsToResolve = new ArrayList<>(commentListAdapter.getClaimUrlsToResolve());
if (urlsToResolve.size() > 0) { 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 @Override
public void onSuccess(List<Claim> claims) { public void onSuccess(List<Claim> claims) {
if (commentListAdapter != null) { if (commentListAdapter != null) {

View file

@ -70,6 +70,7 @@ public final class Helper {
public static final String METHOD_GET = "GET"; public static final String METHOD_GET = "GET";
public static final String METHOD_POST = "POST"; 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_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 String SDK_AMOUNT_FORMAT = "0.0#######";
public static final MediaType FORM_MEDIA_TYPE = MediaType.parse("application/x-www-form-urlencoded"); 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"); public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8");

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<path
android:fillColor="#FF000000"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/red" />
<corners android:radius="16dp" />
</shape>

View file

@ -28,20 +28,22 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_marginRight="24dp">
<EditText <EditText
android:id="@+id/wunderbar" android:id="@+id/wunderbar"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:layout_centerVertical="true"
android:drawableLeft="@drawable/ic_search" android:drawableLeft="@drawable/ic_search"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:drawableTint="@color/actionBarForeground" android:drawableTint="@color/actionBarForeground"
android:fontFamily="@font/inter" android:fontFamily="@font/inter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginRight="24dp"
android:hint="@string/uri_placeholder" android:hint="@string/uri_placeholder"
android:imeOptions="actionGo" android:imeOptions="actionGo"
android:inputType="textNoSuggestions" android:inputType="textNoSuggestions"
android:paddingRight="36dp"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:singleLine="true" android:singleLine="true"
android:textFontWeight="300" android:textFontWeight="300"
@ -52,6 +54,7 @@
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginRight="24dp"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:visibility="gone"> android:visibility="gone">
@ -61,6 +64,39 @@
android:src="@drawable/ic_close" android:src="@drawable/ic_close"
android:tint="@color/actionBarForeground" /> android:tint="@color/actionBarForeground" />
</LinearLayout> </LinearLayout>
<RelativeLayout
android:id="@+id/wunderbar_notifications"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:layout_width="48dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_centerVertical="true">
<ImageView
android:id="@+id/notifications_toggle_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerInParent="true"
android:src="@drawable/ic_notifications"
android:tint="@color/actionBarForeground" />
<TextView
android:id="@+id/notifications_badge_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_notification_badge"
android:fontFamily="@font/inter"
android:gravity="center_horizontal"
android:minWidth="12dp"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:layout_marginTop="12dp"
android:layout_marginLeft="24dp"
android:textColor="@color/white"
android:textSize="10sp"
android:visibility="invisible"
/>
</RelativeLayout>
</RelativeLayout> </RelativeLayout>
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View file

@ -33,6 +33,53 @@
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</RelativeLayout> </RelativeLayout>
<RelativeLayout
android:id="@+id/notifications_container"
android:background="@color/pageBackground"
android:elevation="6dp"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<ProgressBar
android:id="@+id/notifications_progress"
android:layout_centerInParent="true"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/notification_list_empty_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="36dp"
android:visibility="gone">
<ImageView
android:layout_gravity="center_horizontal"
android:layout_width="160dp"
android:layout_height="300dp"
android:adjustViewBounds="true"
android:src="@drawable/gerbil_happy" />
<TextView
android:text="@string/no_notifications"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_gravity="center_horizontal"
android:fontFamily="@font/inter"
android:textSize="16sp"
android:textAlignment="center" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/notifications_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:clipToPadding="false"
/>
</RelativeLayout>
<include layout="@layout/floating_wallet_balance" /> <include layout="@layout/floating_wallet_balance" />
<RelativeLayout <RelativeLayout

View file

@ -721,19 +721,6 @@
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" /> android:visibility="gone" />
<TextView
android:id="@+id/file_view_comments_sdk_initializing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/inter"
android:text="@string/sdk_initializing_comments"
android:textFontWeight="300"
android:textSize="14sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/file_view_comments_list" android:id="@+id/file_view_comments_list"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<io.lbry.browser.ui.controls.SolidIconView
android:id="@+id/notification_icon"
android:layout_marginLeft="16dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:textSize="24dp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/notification_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:fontFamily="@font/inter"
android:textStyle="bold"
android:textSize="13sp" />
<TextView
android:id="@+id/notification_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:fontFamily="@font/inter"
android:textSize="14sp" />
<TextView
android:id="@+id/notification_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -138,6 +138,13 @@
android:src="@drawable/ic_check" android:src="@drawable/ic_check"
android:tint="@color/nextLbryGreen" /> android:tint="@color/nextLbryGreen" />
</RelativeLayout> </RelativeLayout>
<LinearLayout
android:id="@+id/claim_thumbnail_placeholder"
android:background="@color/lighterGrey"
android:orientation="vertical"
android:layout_width="160dp"
android:layout_height="90dp"
android:visibility="gone" />
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout
@ -146,6 +153,22 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_marginLeft="16dp" android:layout_marginLeft="16dp"
android:layout_toRightOf="@id/claim_media_container"> android:layout_toRightOf="@id/claim_media_container">
<LinearLayout
android:id="@+id/claim_text_loading_placeholder_1"
android:background="@color/lighterGrey"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="16dp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/claim_text_loading_placeholder_2"
android:background="@color/lighterGrey"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_marginTop="8dp"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/claim_vanity_url" android:id="@+id/claim_vanity_url"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -603,6 +603,9 @@
<string name="cannot_find_lbrynet_log">The lbrynet.log file could not be found.</string> <string name="cannot_find_lbrynet_log">The lbrynet.log file could not be found.</string>
<string name="cannot_share_lbrynet_log">The lbrynet.log file cannot be shared due to permission restrictions.</string> <string name="cannot_share_lbrynet_log">The lbrynet.log file cannot be shared due to permission restrictions.</string>
<!-- Notifications -->
<string name="no_notifications">It\'s quiet here! New notifications will be displayed when you receive them.</string>
<!-- Font Awesome --> <!-- Font Awesome -->
<string name="fa_gift" translatable="false">&#xf06b;</string> <string name="fa_gift" translatable="false">&#xf06b;</string>
<string name="fa_lock" translatable="false">&#xf023;</string> <string name="fa_lock" translatable="false">&#xf023;</string>
@ -626,4 +629,7 @@
<string name="fa_mobile_alt" translatable="false">&#xf3cd;</string> <string name="fa_mobile_alt" translatable="false">&#xf3cd;</string>
<string name="fa_repost" translatable="false">&#xf079;</string> <string name="fa_repost" translatable="false">&#xf079;</string>
<string name="fa_folder_open" translatable="false">&#xf07c;</string> <string name="fa_folder_open" translatable="false">&#xf07c;</string>
<string name="fa_asterisk" translatable="false">&#xf069;</string>
<string name="fa_comment_alt" translatable="false">&#xf27a;</string>
</resources> </resources>

View file

@ -7,6 +7,7 @@
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="io.lbry.browser.preference.userinterface.BackgroundPlayback" app:key="io.lbry.browser.preference.userinterface.BackgroundPlayback"
app:defaultValue="true"
app:title="@string/enable_background_playback" app:title="@string/enable_background_playback"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
@ -27,6 +28,11 @@
<PreferenceCategory <PreferenceCategory
android:title="@string/notifications" android:title="@string/notifications"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<SwitchPreferenceCompat
app:key="io.lbry.browser.preference.notifications.Comments"
app:title="@string/comments"
app:defaultValue="true"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="io.lbry.browser.preference.notifications.Subscriptions" app:key="io.lbry.browser.preference.notifications.Subscriptions"
app:title="@string/subscriptions" app:title="@string/subscriptions"