Finish Following and All Content views. Add customize your tags view.

This commit is contained in:
Akinwale Ariwodola 2020-04-30 19:05:37 +01:00
parent 59584c1be7
commit 1d1c761d3f
41 changed files with 1761 additions and 169 deletions

View file

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.AsyncTask;
@ -18,6 +19,7 @@ import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.widget.NestedScrollView;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -292,12 +294,14 @@ public class FileViewActivity extends AppCompatActivity {
TagListAdapter tagListAdapter = new TagListAdapter(tags, this);
tagListAdapter.setClickListener(new TagListAdapter.TagClickListener() {
@Override
public void onTagClicked(Tag tag) {
public void onTagClicked(Tag tag, int customizeMode) {
if (customizeMode == TagListAdapter.CUSTOMIZE_MODE_NONE) {
Intent intent = new Intent(MainActivity.ACTION_OPEN_ALL_CONTENT_TAG);
intent.putExtra("tag", tag.getName());
sendBroadcast(intent);
moveTaskToBack(true);
}
}
});
descTagsList.setAdapter(tagListAdapter);
findViewById(R.id.file_view_tag_area).setVisibility(tags.size() > 0 ? View.VISIBLE : View.GONE);
@ -378,7 +382,11 @@ public class FileViewActivity extends AppCompatActivity {
String title = claim.getTitle();
String claimId = claim.getClaimId();
ProgressBar relatedLoading = findViewById(R.id.file_view_related_content_progress);
LighthouseSearchTask relatedTask = new LighthouseSearchTask(title, RELATED_CONTENT_SIZE, 0, false, claimId, relatedLoading, new ClaimSearchTask.ClaimSearchResultHandler() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false);
LighthouseSearchTask relatedTask = new LighthouseSearchTask(
title, RELATED_CONTENT_SIZE, 0, canShowMatureContent, claimId, relatedLoading, new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
List<Claim> filteredClaims = new ArrayList<>();

View file

@ -12,6 +12,7 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
@ -20,7 +21,6 @@ import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.inputmethod.InputMethodManager;
@ -63,6 +63,7 @@ import java.io.InputStreamReader;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -73,12 +74,13 @@ import java.util.concurrent.TimeUnit;
import io.lbry.browser.adapter.NavigationMenuAdapter;
import io.lbry.browser.adapter.UrlSuggestionListAdapter;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.WalletBalanceListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.ClaimCacheKey;
import io.lbry.browser.model.NavMenuItem;
import io.lbry.browser.model.Tag;
import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.model.WalletSync;
@ -87,7 +89,6 @@ import io.lbry.browser.tasks.LighthouseAutoCompleteTask;
import io.lbry.browser.tasks.ResolveTask;
import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler;
import io.lbry.browser.tasks.wallet.SyncGetTask;
import io.lbry.browser.tasks.wallet.SyncTaskHandler;
import io.lbry.browser.tasks.wallet.WalletBalanceTask;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.ui.channel.ChannelFragment;
@ -103,6 +104,7 @@ import io.lbry.browser.utils.Lbryio;
import io.lbry.lbrysdk.LbrynetService;
import io.lbry.lbrysdk.ServiceHelper;
import io.lbry.lbrysdk.Utils;
import lombok.Data;
import lombok.Getter;
public class MainActivity extends AppCompatActivity implements SdkStatusListener {
@ -141,6 +143,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
// preference keys
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_NOTIFICATION_URL_SUGGESTIONS = "io.lbry.browser.preference.userinterface.UrlSuggestions";
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";
@ -164,6 +167,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
private NavigationMenuAdapter navMenuAdapter;
private UrlSuggestionListAdapter urlSuggestionListAdapter;
private List<UrlSuggestion> recentHistory;
// broadcast receivers
private BroadcastReceiver serviceActionsReceiver;
@ -526,10 +530,18 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
private void toggleUrlSuggestions(boolean visible) {
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);
container.setVisibility(visible ? View.VISIBLE : View.GONE);
closeIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private int getScaledValue(int value) {
float scale = getResources().getDisplayMetrics().density;
return (int) (value * scale + 0.5f);
}
private void setupUriBar() {
findViewById(R.id.wunderbar_close).setOnClickListener(new View.OnClickListener() {
@Override
@ -543,6 +555,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
@Override
public void onFocusChange(View view, boolean hasFocus) {
toggleUrlSuggestions(hasFocus);
if (hasFocus && Helper.isNullOrEmpty(Helper.getValue(((EditText) view).getText()))) {
displayUrlSuggestionsForNoInput();
}
}
});
@ -572,6 +587,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
switch (urlSuggestion.getType()) {
case UrlSuggestion.TYPE_CHANNEL:
// open channel page
openChannelUrl(urlSuggestion.getUri().toString());
break;
case UrlSuggestion.TYPE_FILE:
Context context = MainActivity.this;
@ -586,6 +602,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
break;
case UrlSuggestion.TYPE_TAG:
// open tag page
openAllContentFragmentWithTag(urlSuggestion.getText());
break;
}
findViewById(R.id.wunderbar).clearFocus();
@ -618,6 +635,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
ResolveTask task = new ResolveTask(urls, Lbry.LBRY_TV_CONNECTION_STRING, null, new ResolveTask.ResolveResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) {
for (int i = 0; i < claims.size(); i++) {
// build a simple url from the claim for matching
Claim claim = claims.get(i);
@ -637,6 +655,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
}
urlSuggestionListAdapter.notifyDataSetChanged();
}
}
@Override
public void onError(Exception error) {
@ -646,10 +665,19 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
private void displayUrlSuggestionsForNoInput() {
urlSuggestionListAdapter.clear();
List<UrlSuggestion> blankSuggestions = buildDefaultSuggestionsForBlankUrl();
urlSuggestionListAdapter.addUrlSuggestions(blankSuggestions);
List<String> urls = urlSuggestionListAdapter.getItemUrls();
resolveUrlSuggestions(urls);
}
private void handleUriInputChanged(String text) {
// build the default suggestions
urlSuggestionListAdapter.clear();
if (Helper.isNullOrEmpty(text)) {
if (Helper.isNullOrEmpty(text) || text.trim().equals("@")) {
displayUrlSuggestionsForNoInput();
return;
}
@ -659,11 +687,13 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
LighthouseAutoCompleteTask task = new LighthouseAutoCompleteTask(text, null, new LighthouseAutoCompleteTask.AutoCompleteResultHandler() {
@Override
public void onSuccess(List<UrlSuggestion> suggestions) {
String wunderBarText = Helper.getValue(((EditText) findViewById(R.id.wunderbar)).getText());
if (wunderBarText.equalsIgnoreCase(text)) {
urlSuggestionListAdapter.addUrlSuggestions(suggestions);
List<String> urls = urlSuggestionListAdapter.getItemUrls();
resolveUrlSuggestions(urls);
}
}
@Override
public void onError(Exception error) {
@ -673,12 +703,37 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private List<UrlSuggestion> buildDefaultSuggestionsForBlankUrl() {
List<UrlSuggestion> suggestions = new ArrayList<>();
if (recentHistory != null && recentHistory.size() > 0) {
// show recent history if avaiable
suggestions = new ArrayList<>(recentHistory);
} else {
try {
suggestions.add(new UrlSuggestion(
UrlSuggestion.TYPE_FILE, "What is LBRY?", LbryUri.parse("lbry://what#19b9c243bea0c45175e6a6027911abbad53e983e")));
suggestions.add(new UrlSuggestion(
UrlSuggestion.TYPE_CHANNEL, "LBRYCast", LbryUri.parse("lbry://@lbrycast#4c29f8b013adea4d5cca1861fb2161d5089613ea")));
suggestions.add(new UrlSuggestion(
UrlSuggestion.TYPE_CHANNEL, "The LBRY Channel", LbryUri.parse("lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a")));
for (UrlSuggestion suggestion : suggestions) {
suggestion.setUseTextAsDescription(true);
}
} catch (LbryUriException ex) {
// pass
}
}
return suggestions;
}
private List<UrlSuggestion> buildDefaultSuggestions(String text) {
List<UrlSuggestion> suggestions = new ArrayList<UrlSuggestion>();
// First item is always search
if (!text.startsWith(LbryUri.PROTO_DEFAULT)) {
UrlSuggestion searchSuggestion = new UrlSuggestion(UrlSuggestion.TYPE_SEARCH, text);
suggestions.add(searchSuggestion);
}
if (!text.matches(LbryUri.REGEX_INVALID_URI)) {
boolean isChannel = text.startsWith("@");
@ -990,6 +1045,12 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
return false;
}
SQLiteDatabase db = dbHelper.getReadableDatabase();
List<Tag> fetchedTags = DatabaseHelper.getTags(db);
Lbry.knownTags = Helper.mergeKnownTags(fetchedTags);
Collections.sort(Lbry.knownTags, new Tag());
Lbry.followedTags = Helper.filterFollowedTags(Lbry.knownTags);
// load the exchange rate
if (Lbryio.LBCUSDRate == 0) {
Lbryio.loadExchangeRate();
@ -1175,7 +1236,6 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
findContentGroup.setItems(Arrays.asList(
new NavMenuItem(NavMenuItem.ID_ITEM_FOLLOWING, R.string.fa_heart, R.string.following, "Following", context),
new NavMenuItem(NavMenuItem.ID_ITEM_EDITORS_CHOICE, R.string.fa_star, R.string.editors_choice, "EditorsChoice", context),
new NavMenuItem(NavMenuItem.ID_ITEM_YOUR_TAGS, R.string.fa_hashtag, R.string.your_tags, "YourTags", context),
new NavMenuItem(NavMenuItem.ID_ITEM_ALL_CONTENT, R.string.fa_globe_americas, R.string.all_content, "AllContent", context)
));
@ -1421,4 +1481,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void loadTags() {
}
}

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
@ -13,23 +14,35 @@ import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.Tag;
import lombok.Getter;
import lombok.Setter;
public class TagListAdapter extends RecyclerView.Adapter<TagListAdapter.ViewHolder> {
public static final int CUSTOMIZE_MODE_NONE = 0;
public static final int CUSTOMIZE_MODE_ADD = 1;
public static final int CUSTOMIZE_MODE_REMOVE = 2;
private Context context;
private List<Tag> items;
@Setter
private TagClickListener clickListener;
@Getter
@Setter
private int customizeMode;
public TagListAdapter(List<Tag> tags, Context context) {
this.context = context;
this.items = new ArrayList<>(tags);
this.customizeMode = CUSTOMIZE_MODE_NONE;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected ImageView iconView;
protected TextView nameView;
public ViewHolder(View v) {
super(v);
iconView = v.findViewById(R.id.tag_action);
nameView = v.findViewById(R.id.tag_name);
}
}
@ -38,6 +51,21 @@ public class TagListAdapter extends RecyclerView.Adapter<TagListAdapter.ViewHold
return items != null ? items.size() : 0;
}
public void addTag(Tag tag) {
if (!items.contains(tag)) {
items.add(tag);
}
notifyDataSetChanged();
}
public List<Tag> getTags() {
return new ArrayList<>(items);
}
public void setTags(List<Tag> tags) {
items = new ArrayList<>(tags);
notifyDataSetChanged();
}
public void addTags(List<Tag> tags) {
for (Tag tag : tags) {
if (!items.contains(tag)) {
@ -46,6 +74,10 @@ public class TagListAdapter extends RecyclerView.Adapter<TagListAdapter.ViewHold
}
notifyDataSetChanged();
}
public void removeTag(Tag tag) {
items.remove(tag);
notifyDataSetChanged();
}
@Override
public TagListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
@ -57,18 +89,20 @@ public class TagListAdapter extends RecyclerView.Adapter<TagListAdapter.ViewHold
public void onBindViewHolder(TagListAdapter.ViewHolder vh, int position) {
Tag tag = items.get(position);
vh.nameView.setText(tag.getName().toLowerCase());
vh.iconView.setVisibility(customizeMode == CUSTOMIZE_MODE_NONE ? View.GONE : View.VISIBLE);
vh.iconView.setImageResource(customizeMode == CUSTOMIZE_MODE_REMOVE ? R.drawable.ic_close : R.drawable.ic_add);
vh.itemView.setBackgroundResource(tag.isMature() ? R.drawable.bg_tag_mature : R.drawable.bg_tag);
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (clickListener != null) {
clickListener.onTagClicked(tag);
clickListener.onTagClicked(tag, customizeMode);
}
}
});
}
public interface TagClickListener {
void onTagClicked(Tag tag);
void onTagClicked(Tag tag, int customizeMode);
}
}

View file

@ -12,6 +12,7 @@ import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.ui.controls.SolidIconView;
@ -52,10 +53,17 @@ public class UrlSuggestionListAdapter extends RecyclerView.Adapter<UrlSuggestion
public void setClaimForUrl(LbryUri url, Claim claim) {
for (int i = 0; i < items.size(); i++) {
LbryUri thisUrl = items.get(i).getUri();
if (thisUrl != null && thisUrl.equals(url)) {
try {
if (thisUrl != null) {
LbryUri vanity = LbryUri.parse(thisUrl.toVanityString());
if (thisUrl.equals(url) || vanity.equals(url)) {
items.get(i).setClaim(claim);
}
}
} catch (LbryUriException ex) {
// pass
}
}
}
public void addUrlSuggestions(List<UrlSuggestion> urlSuggestions) {
@ -88,7 +96,7 @@ public class UrlSuggestionListAdapter extends RecyclerView.Adapter<UrlSuggestion
iconStringId = R.string.fa_at;
fullTitle = item.getTitle();
desc = item.getClaim() != null ? item.getClaim().getTitle() :
String.format(context.getString(R.string.view_channel_url_desc), item.getText());
(item.isUseTextAsDescription() ? item.getText() : String.format(context.getString(R.string.view_channel_url_desc), item.getText()));
break;
case UrlSuggestion.TYPE_TAG:
iconStringId = R.string.fa_hashtag;
@ -105,7 +113,7 @@ public class UrlSuggestionListAdapter extends RecyclerView.Adapter<UrlSuggestion
iconStringId = R.string.fa_file;
fullTitle = item.getTitle();
desc = item.getClaim() != null ? item.getClaim().getTitle() :
String.format(context.getString(R.string.view_file_url_desc), item.getText());
(item.isUseTextAsDescription() ? item.getText() : String.format(context.getString(R.string.view_file_url_desc), item.getText()));
break;
}

View file

@ -5,11 +5,17 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.text.SimpleDateFormat;
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.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 = 1;
@ -18,18 +24,34 @@ public class DatabaseHelper extends SQLiteOpenHelper {
private static final String[] SQL_CREATE_TABLES = {
// local subscription store
"CREATE TABLE subscriptions (url TEXT PRIMARY KEY NOT NULL, channel_name TEXT NOT NULL)",
// local claim cache store for quick load / refresh
// url entry / suggestion history
"CREATE TABLE history (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL, url TEXT, type INTEGER NOT NULL, timestamp TEXT NOT NULL)",
// tags (known and followed)
"CREATE TABLE tags (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, is_followed INTEGER NOT NULL)"
// local claim cache store for quick load / refresh (or offline mode)?
};
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)",
"CREATE UNIQUE INDEX idx_history_value ON history (value)",
"CREATE UNIQUE INDEX idx_history_url ON history (url)",
"CREATE UNIQUE INDEX idx_tag_name ON tags (name)"
};
private static final String SQL_INSERT_SUBSCRIPTION = "REPLACE INTO subscriptions (channel_name, url) VALUES (?, ?)";
private static final String SQL_DELETE_SUBSCRIPTION = "DELETE FROM subscriptions WHERE url = ?";
private static final String SQL_GET_SUBSCRIPTIONS = "SELECT channel_name, url FROM subscriptions";
private static final String SQL_INSERT_HISTORY = "REPLACE INTO history (value, url, type, timestamp) VALUES (?, ?, ?)";
private static final String SQL_CLEAR_HISTORY = "DELETE FROM history";
private static final String SQL_CLEAR_HISTORY_BEFORE_TIME = "DELETE FROM history WHERE timestamp < ?";
private static final String SQL_GET_RECENT_HISTORY = "SELECT value, url, type FROM history ORDER BY timestamp DESC LIMIT 10";
private static final String SQL_INSERT_TAG = "REPLACE INTO tags (name, is_followed) VALUES (?, ?)";
private static final String SQL_SET_TAG_FOLLOWED = "UPDATE tags SET is_followed = ? WHERE name = ?";
private static final String SQL_GET_KNOWN_TAGS = "SELECT name FROM tags";
private static final String SQL_GET_FOLLOWED_TAGS = "SELECT name FROM tags WHERE is_followed = 1";
public DatabaseHelper(Context context) {
super(context, String.format("%s/%s", context.getFilesDir().getAbsolutePath(), DATABASE_NAME), null, DATABASE_VERSION);
}
@ -48,6 +70,64 @@ public class DatabaseHelper extends SQLiteOpenHelper {
}
public static void createOrUpdateHistoryItem(String text, String url, int type, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_HISTORY, new Object[] {
text, url, type, new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date())
});
}
public static void clearHistory(SQLiteDatabase db) {
db.execSQL(SQL_CLEAR_HISTORY);
}
public static void clearHistoryBefore(Date date, SQLiteDatabase db) {
db.execSQL(SQL_CLEAR_HISTORY_BEFORE_TIME, new Object[] { new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date()) });
}
// History items are essentially url suggestions
public static List<UrlSuggestion> getRecentHistory(SQLiteDatabase db) {
List<UrlSuggestion> suggestions = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_RECENT_HISTORY, null);
while (cursor.moveToNext()) {
UrlSuggestion suggestion = new UrlSuggestion();
suggestion.setText(cursor.getString(0));
suggestion.setType(cursor.getInt(2));
try {
suggestion.setUri(cursor.isNull(1) ? null : LbryUri.parse(cursor.getString(1)));
} catch (LbryUriException ex) {
// don't fail if the LbryUri is invalid
}
suggestions.add(suggestion);
}
} finally {
Helper.closeCursor(cursor);
}
return suggestions;
}
public static void createOrUpdateTag(Tag tag, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_TAG, new Object[] { tag.getLowercaseName(), tag.isFollowed() ? 1 : 0 });
}
public static void setTagFollowed(boolean followed, String name, SQLiteDatabase db) {
db.execSQL(SQL_SET_TAG_FOLLOWED, new Object[] { followed ? 1 : 0, name });
}
public static List<Tag> getTags(SQLiteDatabase db) {
List<Tag> tags = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_KNOWN_TAGS, null);
while (cursor.moveToNext()) {
Tag tag = new Tag();
tag.setName(cursor.getString(0));
tag.setFollowed(cursor.getInt(1) == 1);
tags.add(tag);
}
} finally {
Helper.closeCursor(cursor);
}
return tags;
}
public static void createOrUpdateSubscription(Subscription subscription, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_SUBSCRIPTION, new Object[] { subscription.getChannelName(), subscription.getUrl() });
}

View file

@ -0,0 +1,96 @@
package io.lbry.browser.dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import io.lbry.browser.R;
import lombok.Setter;
public class ContentScopeDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = "ContentScopeDialog";
public static final int ITEM_EVERYONE = 1;
public static final int ITEM_TAGS = 2;
@Setter
private ContentScopeListener contentScopeListener;
private int currentScopeItem;
public static ContentScopeDialogFragment newInstance() {
return new ContentScopeDialogFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_content_scope, container,false);
ContentScopeItemClickListener clickListener = new ContentScopeItemClickListener(this, contentScopeListener);
view.findViewById(R.id.content_scope_everyone_item).setOnClickListener(clickListener);
view.findViewById(R.id.content_scope_tags_item).setOnClickListener(clickListener);
checkSelectedScopeItem(currentScopeItem, view);
return view;
}
public static void checkSelectedScopeItem(int scope, View parent) {
int checkViewId = -1;
switch (scope) {
case ITEM_EVERYONE: checkViewId = R.id.content_scope_everyone_item_selected; break;
case ITEM_TAGS: checkViewId = R.id.content_scope_tags_item_selected; break;
}
if (parent != null && checkViewId > -1) {
parent.findViewById(checkViewId).setVisibility(View.VISIBLE);
}
}
public void setCurrentScopeItem(int scopeItem) {
this.currentScopeItem = scopeItem;
}
private static class ContentScopeItemClickListener implements View.OnClickListener {
private final int[] checkViewIds = {
R.id.content_scope_everyone_item_selected, R.id.content_scope_tags_item_selected
};
private BottomSheetDialogFragment dialog;
private ContentScopeListener listener;
public ContentScopeItemClickListener(BottomSheetDialogFragment dialog, ContentScopeListener listener) {
this.dialog = dialog;
this.listener = listener;
}
public void onClick(View view) {
int scopeItem = -1;
if (dialog != null) {
View dialogView = dialog.getView();
if (dialogView != null) {
for (int id : checkViewIds) {
dialogView.findViewById(id).setVisibility(View.GONE);
}
}
}
switch (view.getId()) {
case R.id.content_scope_everyone_item: scopeItem = ITEM_EVERYONE; break;
case R.id.content_scope_tags_item: scopeItem = ITEM_TAGS; break;
}
checkSelectedScopeItem(scopeItem, view);
if (listener != null) {
listener.onContentScopeItemSelected(scopeItem);
}
if (dialog != null) {
dialog.dismiss();
}
}
}
public interface ContentScopeListener {
void onContentScopeItemSelected(int scopeItem);
}
}

View file

@ -0,0 +1,211 @@
package io.lbry.browser.dialog;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.flexbox.FlexboxLayoutManager;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import io.lbry.browser.R;
import io.lbry.browser.adapter.TagListAdapter;
import io.lbry.browser.model.Tag;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import lombok.Setter;
public class CustomizeTagsDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = "CustomizeTagsDialog";
private static final int SUGGESTED_LIMIT = 8;
private String currentFilter;
private RecyclerView followedTagsList;
private RecyclerView suggestedTagsList;
private TagListAdapter followedTagsAdapter;
private TagListAdapter suggestedTagsAdapter;
private View noTagsView;
private View noResultsView;
@Setter
private TagListener listener;
private void checkNoTags() {
Helper.setViewVisibility(noTagsView, followedTagsAdapter == null || followedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
private void checkNoResults() {
Helper.setViewVisibility(noResultsView, suggestedTagsAdapter == null || suggestedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
public void addTag(Tag tag) {
if (followedTagsAdapter.getTags().contains(tag)) {
Snackbar.make(getView(), getString(R.string.tag_already_followed, tag.getName()), Snackbar.LENGTH_LONG).show();
return;
}
tag.setFollowed(true);
followedTagsAdapter.addTag(tag);
if (suggestedTagsAdapter != null) {
suggestedTagsAdapter.removeTag(tag);
}
updateKnownTags(currentFilter, SUGGESTED_LIMIT, false);
if (listener != null) {
listener.onTagAdded(tag);
}
checkNoTags();
checkNoResults();
}
public void removeTag(Tag tag) {
tag.setFollowed(false);
followedTagsAdapter.removeTag(tag);
updateKnownTags(currentFilter, SUGGESTED_LIMIT, false);
if (listener != null) {
listener.onTagRemoved(tag);
}
checkNoTags();
checkNoResults();
}
public void setFilter(String filter) {
currentFilter = filter;
updateKnownTags(currentFilter, SUGGESTED_LIMIT, true);
}
public static CustomizeTagsDialogFragment newInstance() {
return new CustomizeTagsDialogFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_customize_tags, container, false);
noResultsView = view.findViewById(R.id.customize_no_tag_results);
noTagsView = view.findViewById(R.id.customize_no_followed_tags);
followedTagsAdapter = new TagListAdapter(Lbry.followedTags, getContext());
followedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_REMOVE);
followedTagsAdapter.setClickListener(customizeTagClickListener);
suggestedTagsAdapter = new TagListAdapter(new ArrayList<>(), getContext());
suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD);
suggestedTagsAdapter.setClickListener(customizeTagClickListener);
FlexboxLayoutManager flm1 = new FlexboxLayoutManager(getContext());
followedTagsList = view.findViewById(R.id.customize_tags_followed_list);
followedTagsList.setLayoutManager(flm1);
followedTagsList.setAdapter(followedTagsAdapter);
FlexboxLayoutManager flm2 = new FlexboxLayoutManager(getContext());
suggestedTagsList = view.findViewById(R.id.customize_tags_suggested_list);
suggestedTagsList.setLayoutManager(flm2);
suggestedTagsList.setAdapter(suggestedTagsAdapter);
TextInputEditText filterInput = view.findViewById(R.id.customize_tag_filter_input);
filterInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
String value = Helper.getValue(charSequence);
setFilter(value);
}
@Override
public void afterTextChanged(Editable editable) {
}
});
MaterialButton doneButton = view.findViewById(R.id.customize_done_button);
doneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
}
});
checkNoTags();
return view;
}
private TagListAdapter.TagClickListener customizeTagClickListener = new TagListAdapter.TagClickListener() {
@Override
public void onTagClicked(Tag tag, int customizeMode) {
if (customizeMode == TagListAdapter.CUSTOMIZE_MODE_ADD) {
addTag(tag);
} else if (customizeMode == TagListAdapter.CUSTOMIZE_MODE_REMOVE) {
removeTag(tag);
}
}
};
public void onResume() {
super.onResume();
updateKnownTags(null, SUGGESTED_LIMIT, true);
}
private void updateKnownTags(String filter, int limit, boolean clearPrevious) {
(new AsyncTask<Void, Void, List<Tag>>() {
protected List<Tag> doInBackground(Void... params) {
List<Tag> tags = new ArrayList<>();
if (Helper.isNullOrEmpty(filter)) {
Random random = new Random();
if (suggestedTagsAdapter != null && !clearPrevious) {
tags = new ArrayList<>(suggestedTagsAdapter.getTags());
}
while (tags.size() < limit) {
Tag randomTag = Lbry.knownTags.get(random.nextInt(Lbry.knownTags.size()));
if (!Lbry.followedTags.contains(randomTag) && (followedTagsAdapter == null || !followedTagsAdapter.getTags().contains(randomTag))) {
tags.add(randomTag);
}
}
} else {
Tag filterTag = new Tag(filter);
if (followedTagsAdapter == null || !followedTagsAdapter.getTags().contains(filterTag)) {
tags.add(new Tag(filter));
}
for (int i = 0; i < Lbry.knownTags.size() && tags.size() < SUGGESTED_LIMIT - 1; i++) {
Tag knownTag = Lbry.knownTags.get(i);
if ((knownTag.getLowercaseName().startsWith(filter) || knownTag.getLowercaseName().matches(filter)) &&
(!tags.contains(knownTag) &&
!Lbry.followedTags.contains(knownTag) && (followedTagsAdapter == null || !followedTagsAdapter.getTags().contains(knownTag)))) {
tags.add(knownTag);
}
}
}
return tags;
}
protected void onPostExecute(List<Tag> tags) {
if (suggestedTagsAdapter == null) {
suggestedTagsAdapter = new TagListAdapter(tags, getContext());
suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD);
suggestedTagsAdapter.setClickListener(customizeTagClickListener);
if (suggestedTagsList != null) {
suggestedTagsList.setAdapter(suggestedTagsAdapter);
}
} else {
suggestedTagsAdapter.setTags(tags);
}
checkNoResults();
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public interface TagListener {
void onTagAdded(Tag tag);
void onTagRemoved(Tag tag);
}
}

View file

@ -0,0 +1,26 @@
package io.lbry.browser.model;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
public class ClaimSearchCacheValue {
@Getter
@Setter
private List<Claim> claims;
@Getter
@Setter
private long timestamp;
public ClaimSearchCacheValue(List<Claim> claims, long timestamp) {
this.claims = new ArrayList<>(claims);
this.timestamp = timestamp;
}
public boolean isExpired(long ttl) {
return System.currentTimeMillis() - timestamp > ttl;
}
}

View file

@ -19,8 +19,7 @@ public class NavMenuItem {
// Find Content
public static final int ID_ITEM_FOLLOWING = 101;
public static final int ID_ITEM_EDITORS_CHOICE = 102;
public static final int ID_ITEM_YOUR_TAGS = 103;
public static final int ID_ITEM_ALL_CONTENT = 104;
public static final int ID_ITEM_ALL_CONTENT = 103;
// Your Content
public static final int ID_ITEM_CHANNELS = 201;

View file

@ -1,14 +1,22 @@
package io.lbry.browser.model;
import io.lbry.browser.utils.Helper;
import java.util.Comparator;
import io.lbry.browser.utils.Predefined;
import lombok.Getter;
import lombok.Setter;
public class Tag {
public class Tag implements Comparator<Tag> {
@Getter
@Setter
private String name;
@Getter
@Setter
private boolean followed;
public Tag() {
}
public Tag(String name) {
this.name = name;
}
@ -18,13 +26,16 @@ public class Tag {
}
public boolean isMature() {
return Helper.MATURE_TAG_NAMES.contains(name.toLowerCase());
return Predefined.MATURE_TAGS.contains(name.toLowerCase());
}
public boolean equals(Object o) {
return (o instanceof Tag) && ((Tag) o).getName().equalsIgnoreCase(name);
}
public int hashCode() {
return name.hashCode();
return name.toLowerCase().hashCode();
}
public int compare(Tag a, Tag b) {
return a.getLowercaseName().compareToIgnoreCase(b.getLowercaseName());
}
}

View file

@ -14,21 +14,36 @@ public class UrlSuggestion {
private String text;
private LbryUri uri;
private Claim claim; // associated claim if resolved
private boolean titleTextOnly;
private boolean useTextAsDescription;
public UrlSuggestion() {
}
public UrlSuggestion(int type, String text) {
this.type = type;
this.text = text;
}
public UrlSuggestion(int type, String text, LbryUri uri) {
this(type, text);
this.uri = uri;
}
public UrlSuggestion(int type, String text, LbryUri uri, boolean titleTextOnly) {
this(type, text, uri);
this.titleTextOnly = titleTextOnly;
}
public String getTitle() {
if (!titleTextOnly) {
switch (type) {
case TYPE_CHANNEL:
return String.format("%s - %s", text.startsWith("@") ? text.substring(1) : text, uri.toString());
return String.format("%s - %s", text.startsWith("@") ? text.substring(1) : text, uri.toVanityString());
case TYPE_FILE:
return String.format("%s - %s", text, uri.toString());
return String.format("%s - %s", text, uri.toVanityString());
case TYPE_TAG:
return String.format("%s - #%s", text, text);
}
}
return text;
}

View file

@ -0,0 +1,59 @@
package io.lbry.browser.tasks;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ProgressBar;
import java.util.List;
import io.lbry.browser.MainActivity;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.exceptions.LbryRequestException;
import io.lbry.browser.exceptions.LbryResponseException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.Tag;
import io.lbry.browser.utils.Helper;
public class LoadTagsTask extends AsyncTask<Void, Void, List<Tag>> {
private Context context;
private LoadTagsHandler handler;
private Exception error;
public LoadTagsTask(Context context, LoadTagsHandler handler) {
this.context = context;
this.handler = handler;
}
protected List<Tag> doInBackground(Void... params) {
List<Tag> tags = null;
SQLiteDatabase db = null;
try {
if (context instanceof MainActivity) {
db = ((MainActivity) context).getDbHelper().getReadableDatabase();
if (db != null) {
tags = DatabaseHelper.getTags(db);
}
}
} catch (SQLiteException ex) {
error = ex;
}
return tags;
}
protected void onPostExecute(List<Tag> tags) {
if (handler != null) {
if (tags != null) {
handler.onSuccess(tags);
} else {
handler.onError(error);
}
}
}
public interface LoadTagsHandler {
void onSuccess(List<Tag> tags);
void onError(Exception error);
}
}

View file

@ -2,6 +2,7 @@ package io.lbry.browser.ui.allcontent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -10,9 +11,12 @@ import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -23,19 +27,24 @@ import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.ClaimListAdapter;
import io.lbry.browser.dialog.ContentFromDialogFragment;
import io.lbry.browser.dialog.ContentScopeDialogFragment;
import io.lbry.browser.dialog.ContentSortDialogFragment;
import io.lbry.browser.dialog.CustomizeTagsDialogFragment;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.Tag;
import io.lbry.browser.tasks.ClaimSearchTask;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.Predefined;
// TODO: Similar code to FollowingFragment and Channel page fragment. Probably make common operations (sorting/filtering) into a control
public class AllContentFragment extends BaseFragment {
public class AllContentFragment extends BaseFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
private boolean singleTagView;
private List<String> tags;
private View layoutFilterContainer;
private View customizeLink;
private View sortLink;
private View contentFromLink;
private View scopeLink;
@ -46,13 +55,14 @@ public class AllContentFragment extends BaseFragment {
private RecyclerView contentList;
private int currentSortBy;
private int currentContentFrom;
private int currentScope;
private int currentContentScope;
private String contentReleaseTime;
private List<String> contentSortOrder;
private View fromPrefix;
private View forPrefix;
private View contentLoading;
private View bigContentLoading;
private View noContentView;
private ClaimListAdapter contentListAdapter;
private boolean contentClaimSearchLoading;
private boolean contentHasReachedEnd;
@ -72,6 +82,7 @@ public class AllContentFragment extends BaseFragment {
sortLink = root.findViewById(R.id.all_content_sort_link);
contentFromLink = root.findViewById(R.id.all_content_time_link);
scopeLink = root.findViewById(R.id.all_content_scope_link);
customizeLink = root.findViewById(R.id.all_content_customize_link);
fromPrefix = root.findViewById(R.id.all_content_from_prefix);
forPrefix = root.findViewById(R.id.all_content_for_prefix);
@ -81,6 +92,7 @@ public class AllContentFragment extends BaseFragment {
bigContentLoading = root.findViewById(R.id.all_content_main_progress);
contentLoading = root.findViewById(R.id.all_content_load_progress);
noContentView = root.findViewById(R.id.all_content_no_claim_search_content);
contentList = root.findViewById(R.id.all_content_list);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
@ -127,6 +139,25 @@ public class AllContentFragment extends BaseFragment {
}
}
});
scopeLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ContentScopeDialogFragment dialog = ContentScopeDialogFragment.newInstance();
dialog.setCurrentScopeItem(currentContentScope);
dialog.setContentScopeListener(new ContentScopeDialogFragment.ContentScopeListener() {
@Override
public void onContentScopeItemSelected(int scopeItem) {
onContentScopeChanged(scopeItem);
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), ContentScopeDialogFragment.TAG);
}
}
});
contentFromLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@ -145,6 +176,12 @@ public class AllContentFragment extends BaseFragment {
}
}
});
customizeLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showCustomizeTagsDialog();
}
});
checkParams(false);
@ -188,6 +225,51 @@ public class AllContentFragment extends BaseFragment {
fetchClaimSearchContent(true);
}
private void onContentScopeChanged(int contentScope) {
currentContentScope = contentScope;
// rebuild options and search
updateContentScopeLinkText();
boolean isTagScope = currentContentScope == ContentScopeDialogFragment.ITEM_TAGS;
if (isTagScope) {
tags = Helper.getTagsForTagObjects(Lbry.followedTags);
// Update tags list with the user's followed tags
if (tags == null || tags.size() == 0) {
Snackbar.make(getView(), R.string.customize_tags_hint, Snackbar.LENGTH_LONG).setAction(R.string.customize, new View.OnClickListener() {
@Override
public void onClick(View view) {
// show customize
showCustomizeTagsDialog();
}
}).show();
}
}
Helper.setViewVisibility(customizeLink, isTagScope ? View.VISIBLE : View.GONE);
fetchClaimSearchContent(true);
}
private void showCustomizeTagsDialog() {
CustomizeTagsDialogFragment dialog = CustomizeTagsDialogFragment.newInstance();
dialog.setListener(new CustomizeTagsDialogFragment.TagListener() {
@Override
public void onTagAdded(Tag tag) {
// heavy-lifting
// save to local, save to wallet and then sync
}
@Override
public void onTagRemoved(Tag tag) {
// heavy-lifting
// save to local, save to wallet and then sync
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), CustomizeTagsDialogFragment.TAG);
}
}
private void onSortByChanged(int sortBy) {
currentSortBy = sortBy;
@ -214,6 +296,16 @@ public class AllContentFragment extends BaseFragment {
Helper.setViewText(sortLinkText, stringResourceId);
}
private void updateContentScopeLinkText() {
int stringResourceId = -1;
switch (currentContentScope) {
case ContentScopeDialogFragment.ITEM_EVERYONE: default: stringResourceId = R.string.everyone; break;
case ContentScopeDialogFragment.ITEM_TAGS: stringResourceId = R.string.tags; break;
}
Helper.setViewText(scopeLinkText, stringResourceId);
}
private void updateContentFromLinkText() {
int stringResourceId = -1;
switch (currentContentFrom) {
@ -229,14 +321,26 @@ public class AllContentFragment extends BaseFragment {
public void onResume() {
super.onResume();
PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
updateContentFromLinkText();
updateContentScopeLinkText();
updateSortByLinkText();
fetchClaimSearchContent();
}
public void onPause() {
PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
private Map<String, Object> buildContentOptions() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false);
return Lbry.buildClaimSearchOptions(
Claim.TYPE_STREAM,
tags != null ? tags : null,
null, // TODO: Check mature
(currentContentScope == ContentScopeDialogFragment.ITEM_EVERYONE) ? null : tags,
canShowMatureContent ? null : new ArrayList<>(Predefined.MATURE_TAGS),
null,
null,
getContentSortOrder(),
@ -267,6 +371,7 @@ public class AllContentFragment extends BaseFragment {
}
contentClaimSearchLoading = true;
Helper.setViewVisibility(noContentView, View.GONE);
Map<String, Object> claimSearchOptions = buildContentOptions();
contentClaimSearchTask = new ClaimSearchTask(claimSearchOptions, Lbry.LBRY_TV_CONNECTION_STRING, getLoadingView(), new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
@ -303,13 +408,26 @@ public class AllContentFragment extends BaseFragment {
contentHasReachedEnd = hasReachedEnd;
contentClaimSearchLoading = false;
checkNoContent();
}
@Override
public void onError(Exception error) {
contentClaimSearchLoading = false;
checkNoContent();
}
});
contentClaimSearchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void checkNoContent() {
boolean noContent = contentListAdapter == null || contentListAdapter.getItemCount() == 0;
Helper.setViewVisibility(noContentView, noContent ? View.VISIBLE : View.GONE);
}
public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
if (key.equalsIgnoreCase(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT)) {
fetchClaimSearchContent(true);
}
}
}

View file

@ -2,6 +2,7 @@ package io.lbry.browser.ui.channel;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -11,9 +12,11 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@ -28,9 +31,10 @@ import io.lbry.browser.model.Claim;
import io.lbry.browser.tasks.ClaimSearchTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.Predefined;
import lombok.Setter;
public class ChannelContentFragment extends Fragment {
public class ChannelContentFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener {
@Setter
private String channelId;
@ -45,6 +49,7 @@ public class ChannelContentFragment extends Fragment {
private List<String> contentSortOrder;
private View contentLoading;
private View bigContentLoading;
private View noContentView;
private ClaimListAdapter contentListAdapter;
private boolean contentClaimSearchLoading;
private boolean contentHasReachedEnd;
@ -66,6 +71,7 @@ public class ChannelContentFragment extends Fragment {
bigContentLoading = root.findViewById(R.id.channel_content_main_progress);
contentLoading = root.findViewById(R.id.channel_content_load_progress);
noContentView = root.findViewById(R.id.channel_content_no_claim_search_content);
contentList = root.findViewById(R.id.channel_content_list);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
@ -183,18 +189,23 @@ public class ChannelContentFragment extends Fragment {
public void onResume() {
super.onResume();
PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
fetchClaimSearchContent();
}
public void refresh() {
fetchClaimSearchContent(true);
public void onPause() {
PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
super.onPause();
}
private Map<String, Object> buildContentOptions() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false);
return Lbry.buildClaimSearchOptions(
Claim.TYPE_STREAM,
null,
null, // TODO: Check mature
canShowMatureContent ? null : new ArrayList<>(Predefined.MATURE_TAGS),
Arrays.asList(channelId),
null,
getContentSortOrder(),
@ -225,6 +236,7 @@ public class ChannelContentFragment extends Fragment {
}
contentClaimSearchLoading = true;
Helper.setViewVisibility(noContentView, View.GONE);
Map<String, Object> claimSearchOptions = buildContentOptions();
contentClaimSearchTask = new ClaimSearchTask(claimSearchOptions, Lbry.LBRY_TV_CONNECTION_STRING, getLoadingView(), new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
@ -261,13 +273,26 @@ public class ChannelContentFragment extends Fragment {
contentHasReachedEnd = hasReachedEnd;
contentClaimSearchLoading = false;
checkNoContent();
}
@Override
public void onError(Exception error) {
contentClaimSearchLoading = false;
checkNoContent();
}
});
contentClaimSearchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void checkNoContent() {
boolean noContent = contentListAdapter == null || contentListAdapter.getItemCount() == 0;
Helper.setViewVisibility(noContentView, noContent ? View.VISIBLE : View.GONE);
}
public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
if (key.equalsIgnoreCase(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT)) {
fetchClaimSearchContent(true);
}
}
}

View file

@ -96,7 +96,6 @@ public class ChannelFragment extends BaseFragment {
}
}
if (updateRequired) {
resetFragments();
if (!Helper.isNullOrEmpty(url)) {
resolveUrl();
} else if (claim == null) {
@ -179,25 +178,6 @@ public class ChannelFragment extends BaseFragment {
}).attach();
}
private void resetFragments() {
try {
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) getContext();
FragmentManager manager = activity.getSupportFragmentManager();
FragmentTransaction tx = manager.beginTransaction();
for (Fragment fragment : manager.getFragments()) {
if (fragment.getClass().equals(ChannelAboutFragment.class) || fragment.getClass().equals(ChannelContentFragment.class)) {
tx.remove(fragment);
}
}
tx.commitAllowingStateLoss();
}
} catch (Exception ex) {
// pass
}
}
private static class ChannelPagerAdapter extends FragmentStateAdapter {
private Claim channelClaim;
public ChannelPagerAdapter(Claim channelClaim, FragmentActivity activity) {

View file

@ -2,6 +2,7 @@ package io.lbry.browser.ui.following;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -11,6 +12,8 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -45,8 +48,11 @@ import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Lbryio;
import io.lbry.browser.utils.Predefined;
public class FollowingFragment extends BaseFragment implements FetchSubscriptionsTask.FetchSubscriptionsHandler, ChannelItemSelectionListener {
public class FollowingFragment extends BaseFragment implements
FetchSubscriptionsTask.FetchSubscriptionsHandler,
ChannelItemSelectionListener, SharedPreferences.OnSharedPreferenceChangeListener {
private static final int SUGGESTED_PAGE_SIZE = 45;
private static final int MIN_SUGGESTED_SUBSCRIBE_COUNT = 5;
@ -74,6 +80,7 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
private List<String> contentSortOrder;
private boolean contentClaimSearchLoading = false;
private boolean suggestedClaimSearchLoading = false;
private View noContentView;
private List<Integer> queuedContentPages = new ArrayList<>();
private List<Integer> queuedSuggestedPages = new ArrayList<>();
@ -122,6 +129,7 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
contentLoading = root.findViewById(R.id.following_content_progress);
channelListLoading = root.findViewById(R.id.following_channel_load_progress);
discoverLink = root.findViewById(R.id.following_discover_link);
noContentView = root.findViewById(R.id.following_no_claim_search_content);
Context context = getContext();
GridLayoutManager glm = new GridLayoutManager(context, 3);
@ -318,6 +326,7 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
public void onResume() {
super.onResume();
PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
// check if subscriptions exist
if (suggestedChannelAdapter != null) {
@ -338,6 +347,10 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
fetchSubscriptions();
}
}
public void onPause() {
PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
public void loadFollowing() {
// wrapper to just re-fetch subscriptions (upon user sign in, for example)
@ -350,10 +363,13 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
}
private Map<String, Object> buildSuggestedOptions() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false);
return Lbry.buildClaimSearchOptions(
Claim.TYPE_CHANNEL,
null,
null,
canShowMatureContent ? null : new ArrayList<>(Predefined.MATURE_TAGS),
null,
excludeChannelIdsForDiscover,
Arrays.asList(Claim.ORDER_BY_EFFECTIVE_AMOUNT),
@ -363,10 +379,13 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
}
private Map<String, Object> buildContentOptions() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false);
return Lbry.buildClaimSearchOptions(
Claim.TYPE_STREAM,
null,
null,
canShowMatureContent ? null : new ArrayList<>(Predefined.MATURE_TAGS),
getChannelIds(),
null,
getContentSortOrder(),
@ -514,6 +533,7 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
}
contentClaimSearchLoading = true;
Helper.setViewVisibility(noContentView, View.GONE);
Map<String, Object> claimSearchOptions = buildContentOptions();
contentClaimSearchTask = new ClaimSearchTask(claimSearchOptions, Lbry.LBRY_TV_CONNECTION_STRING, getLoadingView(), new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
@ -551,11 +571,13 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
contentHasReachedEnd = hasReachedEnd;
contentClaimSearchLoading = false;
checkNoContent(false);
}
@Override
public void onError(Exception error) {
contentClaimSearchLoading = false;
checkNoContent(false);
}
});
contentClaimSearchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -571,10 +593,16 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
}
private void fetchSuggestedChannels() {
if (suggestedClaimSearchLoading) {
return;
}
suggestedClaimSearchLoading = true;
if (discoverDialog != null) {
discoverDialog.setLoading(true);
}
Helper.setViewVisibility(noContentView, View.GONE);
suggestedChannelClaimSearchTask = new ClaimSearchTask(
buildSuggestedOptions(),
Lbry.LBRY_TV_CONNECTION_STRING,
@ -600,6 +628,10 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
} else {
suggestedChannelAdapter.addClaims(claims);
}
if (discoverDialog == null || !discoverDialog.isVisible()) {
checkNoContent(true);
}
}
@Override
@ -608,6 +640,9 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
if (discoverDialog != null) {
discoverDialog.setLoading(false);
}
if (discoverDialog == null || !discoverDialog.isVisible()) {
checkNoContent(true);
}
}
});
@ -645,14 +680,22 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
subscription.setUrl(claim.getPermanentUrl());
String channelClaimId = claim.getClaimId();
ChannelSubscribeTask task = new ChannelSubscribeTask(getContext(), channelClaimId, subscription, false, null);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
updateSuggestedDoneButtonText();
ChannelSubscribeTask task = new ChannelSubscribeTask(getContext(), channelClaimId, subscription, false, new ChannelSubscribeTask.ChannelSubscribeHandler() {
@Override
public void onSuccess() {
if (discoverDialog != null) {
fetchSubscriptions();
}
}
@Override
public void onError(Exception exception) {
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
updateSuggestedDoneButtonText();
}
public void onChannelItemDeselected(Claim claim) {
// unsubscribe
Subscription subscription = new Subscription();
@ -660,15 +703,35 @@ public class FollowingFragment extends BaseFragment implements FetchSubscription
subscription.setUrl(claim.getPermanentUrl());
String channelClaimId = claim.getClaimId();
ChannelSubscribeTask task = new ChannelSubscribeTask(getContext(), channelClaimId, subscription, true, null);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
updateSuggestedDoneButtonText();
ChannelSubscribeTask task = new ChannelSubscribeTask(getContext(), channelClaimId, subscription, true, new ChannelSubscribeTask.ChannelSubscribeHandler() {
@Override
public void onSuccess() {
if (discoverDialog != null) {
fetchSubscriptions();
}
}
@Override
public void onError(Exception exception) {
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
updateSuggestedDoneButtonText();
}
public void onChannelSelectionCleared() {
}
private void checkNoContent(boolean suggested) {
RecyclerView.Adapter adpater = suggested ? suggestedChannelAdapter : contentListAdapter;
boolean noContent = adpater == null || adpater.getItemCount() == 0;
Helper.setViewVisibility(noContentView, noContent ? View.VISIBLE : View.GONE);
}
public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
if (key.equalsIgnoreCase(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT)) {
fetchClaimSearchContent(true);
}
}
}

View file

@ -1,5 +1,6 @@
package io.lbry.browser.ui.search;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -10,6 +11,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -29,7 +31,8 @@ import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryUri;
import lombok.Setter;
public class SearchFragment extends BaseFragment implements ClaimListAdapter.ClaimListItemListener {
public class SearchFragment extends BaseFragment implements
ClaimListAdapter.ClaimListItemListener, SharedPreferences.OnSharedPreferenceChangeListener {
private ClaimListAdapter resultListAdapter;
private static final int PAGE_SIZE = 25;
@ -84,18 +87,20 @@ public class SearchFragment extends BaseFragment implements ClaimListAdapter.Cla
public void onResume() {
super.onResume();
if (resultListAdapter == null || resultListAdapter.getItemCount() == 0) {
// new search
PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
if (!Helper.isNullOrEmpty(currentQuery)) {
search(currentQuery, currentFrom);
}
}
if (Helper.isNullOrEmpty(currentQuery)) {
} else {
noQueryView.setVisibility(View.VISIBLE);
noResultsView.setVisibility(View.GONE);
}
}
public void onPause() {
PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
private boolean checkQuery(String query) {
if (!Helper.isNullOrEmpty(query) && !query.equalsIgnoreCase(currentQuery)) {
// new query, reset values
@ -176,7 +181,10 @@ public class SearchFragment extends BaseFragment implements ClaimListAdapter.Cla
}
searchLoading = true;
LighthouseSearchTask task = new LighthouseSearchTask(currentQuery, PAGE_SIZE, currentFrom, false, null, loadingView, new ClaimSearchTask.ClaimSearchResultHandler() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false);
LighthouseSearchTask task = new LighthouseSearchTask(
currentQuery, PAGE_SIZE, currentFrom, canShowMatureContent, null, loadingView, new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
contentHasReachedEnd = hasReachedEnd;
@ -227,4 +235,10 @@ public class SearchFragment extends BaseFragment implements ClaimListAdapter.Cla
MainActivity.openFileClaim(claim, getContext());
}
}
public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
if (key.equalsIgnoreCase(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT)) {
search(currentQuery, currentFrom);
}
}
}

View file

@ -32,14 +32,15 @@ import java.util.Random;
import io.lbry.browser.dialog.ContentFromDialogFragment;
import io.lbry.browser.dialog.ContentSortDialogFragment;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.Tag;
import okhttp3.MediaType;
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 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 List<String> MATURE_TAG_NAMES = Arrays.asList("mature", "nsfw", "porn", "xxx");
public static final int CONTENT_PAGE_SIZE = 25;
public static boolean isNull(String value) {
@ -283,4 +284,43 @@ public final class Helper {
((ColorDrawable) bg).setColor(isPlaceholder ? ContextCompat.getColor(context, android.R.color.transparent) : color);
}
}
public static List<Tag> getTagObjectsForTags(List<String> tags) {
List<Tag> tagObjects = new ArrayList<>(tags.size());
for (String tag : tags) {
tagObjects.add(new Tag(tag));
}
return tagObjects;
}
public static List<String> getTagsForTagObjects(List<Tag> tagObjects) {
List<String> tags = new ArrayList<>(tagObjects.size());
for (Tag tagObject : tagObjects) {
tags.add(tagObject.getLowercaseName());
}
return tags;
}
public static List<Tag> mergeKnownTags(List<Tag> fetchedTags) {
List<Tag> allKnownTags = getTagObjectsForTags(Predefined.DEFAULT_KNOWN_TAGS);
List<Integer> followIndexes = new ArrayList<>();
for (Tag tag : fetchedTags) {
if (!allKnownTags.contains(tag)) {
allKnownTags.add(tag);
} else if (tag.isFollowed()) {
followIndexes.add(allKnownTags.indexOf(tag));
}
}
for (int index : followIndexes) {
allKnownTags.get(index).setFollowed(true);
}
return allKnownTags;
}
public static List<Tag> filterFollowedTags(List<Tag> tags) {
List<Tag> followedTags = new ArrayList<>();
for (Tag tag : followedTags) {
if (tag.isFollowed()) {
followedTags.add(tag);
}
}
return followedTags;
}
}

View file

@ -28,7 +28,9 @@ import io.lbry.browser.exceptions.LbryRequestException;
import io.lbry.browser.exceptions.LbryResponseException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.ClaimCacheKey;
import io.lbry.browser.model.ClaimSearchCacheValue;
import io.lbry.browser.model.File;
import io.lbry.browser.model.Tag;
import io.lbry.browser.model.Transaction;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.model.lbryinc.User;
@ -39,13 +41,16 @@ import okhttp3.RequestBody;
import okhttp3.Response;
public final class Lbry {
public static final String TAG = "Lbry";
public static LinkedHashMap<ClaimCacheKey, Claim> claimCache = new LinkedHashMap<>();
public static LinkedHashMap<Map<String, Object>, List<Claim>> claimSearchCache = new LinkedHashMap<>();
public static LinkedHashMap<Map<String, Object>, ClaimSearchCacheValue> claimSearchCache = new LinkedHashMap<>();
public static WalletBalance walletBalance = new WalletBalance();
public static List<Tag> knownTags = new ArrayList<>();
public static List<Tag> followedTags = new ArrayList<>();
public static final int TTL_CLAIM_SEARCH_VALUE = 120000; // 2-minute TTL for cache
public static final String SDK_CONNECTION_STRING = "http://127.0.0.1:5279";
public static final String LBRY_TV_CONNECTION_STRING = "https://api.lbry.tv/api/v1/proxy";
public static final String TAG = "Lbry";
// Values to obtain from LBRY SDK status
public static boolean IS_STATUS_PARSED = false; // Check if the status has been parsed at least once
@ -360,7 +365,10 @@ public final class Lbry {
public static List<Claim> claimSearch(Map<String, Object> options, String connectionString) throws ApiCallException {
if (claimSearchCache.containsKey(options)) {
return claimSearchCache.get(options);
ClaimSearchCacheValue value = claimSearchCache.get(options);
if (!value.isExpired(TTL_CLAIM_SEARCH_VALUE)) {
return claimSearchCache.get(options).getClaims();
}
}
List<Claim> claims = new ArrayList<>();
@ -377,7 +385,7 @@ public final class Lbry {
}
}
claimSearchCache.put(options, claims);
claimSearchCache.put(options, new ClaimSearchCacheValue(claims, System.currentTimeMillis()));
} catch (LbryRequestException | LbryResponseException | JSONException ex) {
throw new ApiCallException("Could not execute resolve call", ex);
}

View file

@ -68,6 +68,10 @@ public class LbryUri {
}
}
if (components.size() == 0) {
throw new LbryUriException("Regular expression error occurred while trying to parse the value");
}
// components[0] = proto
// components[1] = streamNameOrChannelName
// components[2] = primaryModSeparator
@ -141,7 +145,7 @@ public class LbryUri {
return uri;
}
public String build(boolean includeProto, String protoDefault) {
public String build(boolean includeProto, String protoDefault, boolean vanity) {
String formattedChannelName = null;
if (channelName != null) {
formattedChannelName = channelName.startsWith("@") ? channelName : String.format("@%s", channelName);
@ -162,6 +166,15 @@ public class LbryUri {
primaryClaimId = !Helper.isNullOrEmpty(formattedChannelName) ? channelClaimId : streamClaimId;
}
StringBuilder sb = new StringBuilder();
if (includeProto) {
sb.append(protoDefault);
}
sb.append(primaryClaimName);
if (vanity) {
return sb.toString();
}
String secondaryClaimName = null;
if (Helper.isNullOrEmpty(claimName) && !Helper.isNullOrEmpty(contentName)) {
secondaryClaimName = contentName;
@ -171,11 +184,6 @@ public class LbryUri {
}
String secondaryClaimId = !Helper.isNullOrEmpty(secondaryClaimName) ? streamClaimId : null;
StringBuilder sb = new StringBuilder();
if (includeProto) {
sb.append(protoDefault);
}
sb.append(primaryClaimName);
if (!Helper.isNullOrEmpty(primaryClaimId)) {
sb.append('#').append(primaryClaimId);
}
@ -205,8 +213,11 @@ public class LbryUri {
return parse(url).toString();
}
public String toVanityString() {
return build(true, PROTO_DEFAULT, true);
}
public String toString() {
return build(true, PROTO_DEFAULT);
return build(true, PROTO_DEFAULT, false);
}
public int hashCode() {
return toString().hashCode();

View file

@ -0,0 +1,492 @@
package io.lbry.browser.utils;
import java.util.Arrays;
import java.util.List;
public final class Predefined {
public static final List<String> DEFAULT_KNOWN_TAGS = Arrays.asList(
"free speech",
"censorship",
"gaming",
"pop culture",
"entertainment",
"technology",
"music",
"funny",
"education",
"learning",
"news",
"gameplay",
"nature",
"beliefs",
"comedy",
"games",
"film & animation",
"whothinks",
"game",
"weapons",
"blockchain",
"video game",
"sports",
"walkthrough",
"art",
"pc",
"minecraft",
"playthrough",
"economics",
"automotive",
"play",
"tutorial",
"twitch",
"how to",
"ps4",
"bitcoin",
"fortnite",
"commentary",
"lets play",
"fun",
"politics",
"travel",
"food",
"science",
"xbox",
"liberal",
"democrat",
"progressive",
"survival",
"non-profits",
"activism",
"cryptocurrency",
"playstation",
"nintendo",
"government",
"steam",
"podcast",
"gamer",
"horror",
"conservative",
"reaction",
"trailer",
"love",
"cnn",
"republican",
"political",
"hangoutsonair",
"hoa",
"msnbc",
"cbs",
"anime",
"donald trump",
"fiction",
"fox news",
"crypto",
"ethereum",
"call of duty",
"android",
"multiplayer",
"epic",
"rpg",
"adventure",
"secular talk",
"btc",
"atheist",
"atheism",
"video games",
"ps3",
"cod",
"online",
"agnostic",
"movie",
"fps",
"lets",
"mod",
"world",
"reviews",
"sharefactory",
"space",
"pokemon",
"stream",
"hilarious",
"lol",
"sony",
"god",
"dance",
"pvp",
"tech",
"strategy",
"zombies",
"fail",
"film",
"xbox360",
"animation",
"unboxing",
"money",
"wwe",
"mods",
"indie",
"pubg",
"ios",
"history",
"rap",
"mobile",
"trump",
"hack",
"flat earth",
"trap",
"humor",
"vlogging",
"fox",
"news radio",
"facebook",
"edm",
"fitness",
"vaping",
"hip hop",
"secular",
"jesus",
"song",
"vape",
"guitar",
"remix",
"mining",
"daily",
"diy",
"pets",
"videogame",
"death",
"funny moments",
"religion",
"media",
"viral",
"war",
"nbc",
"freedom",
"gold",
"family",
"meme",
"zombie",
"photography",
"chill",
"sniper",
"computer",
"iphone",
"dragon",
"bible",
"pro",
"overwatch",
"litecoin",
"gta",
"house",
"fire",
"bass",
"truth",
"crash",
"mario",
"league of legends",
"wii",
"mmorpg",
"health",
"marvel",
"racing",
"apple",
"instrumental",
"earth",
"destiny",
"satire",
"race",
"training",
"electronic",
"boss",
"roblox",
"family friendly",
"california",
"react",
"christian",
"mmo",
"twitter",
"help",
"star",
"cars",
"random",
"top 10",
"ninja",
"guns",
"linux",
"lessons",
"vegan",
"future",
"dota 2",
"studio",
"star wars",
"shooting",
"nasa",
"rock",
"league",
"subscribe",
"water",
"gta v",
"car",
"samsung",
"music video",
"skyrim",
"dog",
"comics",
"shooter game",
"bo3",
"halloween",
"liberty",
"eth",
"conspiracy",
"knife",
"fashion",
"stories",
"vapor",
"nvidia",
"cute",
"beat",
"nintendo switch",
"fantasy",
"christmas",
"world of warcraft",
"industry",
"cartoon",
"garden",
"animals",
"windows",
"happy",
"magic",
"memes",
"design",
"tactical",
"fallout 4",
"puzzle",
"parody",
"rv",
"beats",
"building",
"disney",
"drone",
"ps2",
"beach",
"metal",
"christianity",
"business",
"mix",
"bo2",
"cover",
"senate",
"4k",
"united states",
"final",
"hero",
"playing",
"dlc",
"ubisoft",
"halo",
"pc gaming",
"raw",
"investing",
"online learning",
"software",
"ark",
"mojang",
"console",
"battle royale",
"canon",
"microsoft",
"camping",
"ufo",
"progressive talk",
"switch",
"fpv",
"arcade",
"school",
"driving",
"bodybuilding",
"drama",
"retro",
"science fiction",
"eggs",
"australia",
"modded",
"rainbow",
"gamers",
"resident evil",
"drawing",
"brasil",
"england",
"hillary clinton",
"singing",
"final fantasy",
"hiphop",
"video blog",
"mature",
"quad",
"noob",
"simulation",
"illuminati",
"poetry",
"dayz",
"manga",
"howto",
"insane",
"press",
"special",
"church",
"ico",
"weird",
"libertarian",
"crafting",
"level",
"comic",
"sandbox",
"daily vlog",
"outdoor",
"black ops",
"sound",
"christ",
"duty",
"juvenile fiction",
"pc game",
"how-to",
"ww2",
"creepy",
"artist",
"galaxy",
"destiny 2",
"new music",
"quest",
"lee",
"pacman",
"super smash bros",
"day",
"survival horror",
"patreon",
"bitcoin price",
"trending",
"open world",
"wii u",
"dope",
"reaper",
"sniping",
"dubstep",
"truck",
"planet",
"dc",
"amazon",
"spirituality",
"universe",
"video game culture",
"community",
"cat",
"aliens",
"tourism",
"altcoins",
"style",
"travel trailer",
"rda",
"gun",
"secret",
"far cry 5",
"auto",
"culture",
"dj",
"mw2",
"lord",
"full time rving",
"role-playing game",
"prank",
"grand theft auto",
"master",
"wrestling",
"sci-fi",
"workout",
"ghost",
"fake news",
"silly",
"season",
"bo4",
"trading",
"extreme",
"economy",
"combat",
"plays",
"muslim",
"pubg mobile",
"clips",
"bo1",
"paypal",
"sims",
"exploration",
"light",
"ripple",
"paranormal",
"football",
"capcom",
"rta",
"discord",
"batman",
"player",
"server",
"anarchy",
"military",
"playlist",
"cosplay",
"rv park",
"rant",
"edit",
"germany",
"reading",
"chris",
"flash",
"loot",
"bitcoin gratis",
"game reviews",
"movies",
"stupid",
"latest news",
"squad gameplay",
"guru",
"timelapse",
"black ops 3",
"holiday",
"soul",
"motivation",
"mw3",
"vacation",
"sega",
"19th century",
"pop",
"sims 4",
"post",
"smok",
"island",
"scotland",
"paladins",
"warrior",
"creepypasta",
"role-playing",
"solar",
"vr",
"animal",
"peace",
"consciousness",
"dota",
"audio",
"mass effect",
"humour",
"first look",
"videogames",
"future bass",
"freestyle",
"hardcore",
"portugal",
"dantdm",
"teaser",
"lbry",
"coronavirus",
"covidcuts",
"covid-19"
);
public static final List<String> MATURE_TAGS = Arrays.asList("mature", "nsfw", "porn", "xxx");
}

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="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

View file

@ -148,7 +148,8 @@
android:layout_marginRight="24dp"
android:fontFamily="@font/inter"
android:textSize="12sp"
android:textFontWeight="300" />
android:textFontWeight="300"
android:visibility="gone" />
<TextView
android:id="@+id/file_view_publish_time"
android:layout_width="wrap_content"

View file

@ -32,7 +32,6 @@
android:fontFamily="@font/inter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="36dp"
android:hint="@string/uri_placeholder"
android:singleLine="true"
android:textFontWeight="300"

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:text="@string/filter_for"
android:textSize="12sp"
android:textAllCaps="true"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/divider"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp" />
<RelativeLayout
android:id="@+id/content_scope_everyone_item"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<io.lbry.browser.ui.controls.SolidIconView
android:id="@+id/content_scope_everyone_item_icon"
android:layout_width="18dp"
android:layout_height="18dp"
android:textSize="16dp"
android:layout_centerVertical="true"
android:text="@string/fa_globe_americas" />
<TextView
android:layout_toRightOf="@+id/content_scope_everyone_item_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_centerVertical="true"
android:fontFamily="@font/inter"
android:text="@string/everyone"
android:textSize="14sp"
android:textFontWeight="300" />
<ImageView
android:id="@+id/content_scope_everyone_item_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_check"
android:tint="@color/lbryGreen"
android:visibility="gone" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/content_scope_tags_item"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<io.lbry.browser.ui.controls.SolidIconView
android:id="@+id/content_scope_tags_item_icon"
android:layout_width="18dp"
android:layout_height="18dp"
android:textSize="16dp"
android:layout_centerVertical="true"
android:text="@string/fa_hashtag" />
<TextView
android:layout_toRightOf="@+id/content_scope_tags_item_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_centerVertical="true"
android:fontFamily="@font/inter"
android:text="@string/tags_you_follow"
android:textSize="14sp"
android:textFontWeight="300" />
<ImageView
android:id="@+id/content_scope_tags_item_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_check"
android:tint="@color/lbryGreen"
android:visibility="gone" />
</RelativeLayout>
</LinearLayout>

View file

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:text="@string/customize_your_tags"
android:textSize="12sp"
android:textAllCaps="true"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/divider"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/customize_tags_followed_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" />
<TextView
android:id="@+id/customize_no_followed_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="6dp"
android:fontFamily="@font/inter"
android:text="@string/no_followed_tags"
android:textSize="14sp"
android:textFontWeight="300"
android:visibility="gone" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/divider"
android:layout_marginTop="10dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/customize_tags_suggested_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" />
<TextView
android:id="@+id/customize_no_tag_results"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="6dp"
android:fontFamily="@font/inter"
android:text="@string/no_tag_results"
android:textSize="14sp"
android:textFontWeight="300"
android:visibility="gone" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/customize_tag_filter_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_for_more_tags"
android:fontFamily="@font/inter"
android:textSize="14sp"
android:textFontWeight="300" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/customize_done_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/inter"
android:text="@string/done" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -180,6 +180,17 @@
android:visibility="gone" />
</RelativeLayout>
<TextView
android:id="@+id/all_content_no_claim_search_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:fontFamily="@font/inter"
android:text="@string/no_claim_search_content"
android:textSize="14sp"
android:textFontWeight="300"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/all_content_list"
android:clipToPadding="false"

View file

@ -98,6 +98,17 @@
android:layout_alignParentRight="true" />
</RelativeLayout>
<TextView
android:id="@+id/channel_content_no_claim_search_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:fontFamily="@font/inter"
android:text="@string/no_claim_search_content"
android:textSize="14sp"
android:textFontWeight="300"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/channel_content_list"
android:clipToPadding="false"

View file

@ -16,7 +16,6 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
@ -159,6 +158,17 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/following_no_claim_search_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:fontFamily="@font/inter"
android:text="@string/no_claim_search_content"
android:textSize="14sp"
android:textFontWeight="300"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/following_suggested_grid"
android:clipToPadding="false"

View file

@ -30,6 +30,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:fontFamily="@font/inter"
android:textAllCaps="true"
android:textSize="48sp"
android:textColor="@color/white" />
</RelativeLayout>

View file

@ -1,21 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:clickable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="6dp"
android:layout_marginRight="6dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:paddingLeft="12dp"
android:paddingLeft="8dp"
android:paddingRight="12dp"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/tag_action"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginRight="4dp"
android:tint="@color/darkForeground" />
<TextView
android:id="@+id/tag_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:layout_marginLeft="4dp"
android:textSize="12sp"
android:textFontWeight="300" />
</RelativeLayout>
</LinearLayout>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_menu_camera"
android:title="@string/menu_home" />
<item
android:id="@+id/nav_gallery"
android:icon="@drawable/ic_menu_gallery"
android:title="@string/menu_gallery" />
<item
android:id="@+id/nav_slideshow"
android:icon="@drawable/ic_menu_slideshow"
android:title="@string/menu_slideshow" />
</group>
</menu>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

View file

@ -13,6 +13,7 @@
<color name="selectedNavItem">#0E0E0E</color>
<color name="channelCoverBackground">#CC000000</color>
<color name="darkForeground">#EEEEEE</color>
<color name="foreground">#999999</color>
<color name="mediaContainerBackground">#333333</color>
<color name="borderTextArea">#5F5F5F</color>
@ -21,8 +22,8 @@
<color name="brighterLbryGreen">#40B887</color>
<color name="lbryGreen">#2F9176</color>
<color name="nextLbryGreen">#38D9A9</color>
<color name="tagGreen">#E3F6F1</color>
<color name="tagGrape">#77F255DA</color>
<color name="tagGreen">#329A7E</color>
<color name="tagGrape">#77D510B8</color>
<color name="divider">#454545</color>
<color name="lightDivider">#0A0A0A</color>

View file

@ -13,6 +13,7 @@
<color name="selectedNavItem">#F1F1F1</color>
<color name="channelCoverBackground">#CC000000</color>
<color name="darkForeground">#222222</color>
<color name="foreground">#333333</color>
<color name="mediaContainerBackground">#333333</color>
<color name="borderTextArea">#777777</color>

View file

@ -2,21 +2,11 @@
<string name="app_name">LBRY</string>
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>
<string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string>
<string name="menu_home">Home</string>
<string name="menu_gallery">Gallery</string>
<string name="menu_slideshow">Slideshow</string>
<string name="home_second">Home Second</string>
<!-- Nav menu -->
<string name="find_content">Find Content</string>
<string name="your_content">Your Content</string>
<string name="wallet">Wallet</string>
<string name="following">Following</string>
<string name="editors_choice">Editor\'s Choice</string>
<string name="your_tags">Your Tags</string>
@ -29,9 +19,9 @@
<string name="invites">Invites</string>
<string name="settings">Settings</string>
<string name="about">About</string>
<string name="sign_in">Sign In</string>
<string name="startup_failed">App startup failed. Please check your data connection and try again. If this problem persists, please email hello@lbry.com</string>
<string name="no_claim_search_content">No content to display at this time. Please refine your selection or check back later.</string>
<!-- Welcome Page -->
<string name="welcome_to_lbry">Welcome to LBRY.</string>
@ -59,7 +49,6 @@
<!-- File view -->
<string name="tags">Tags</string>
<string name="share">Share</string>
<string name="repost">Repost</string>
<string name="tip">Tip</string>
@ -79,10 +68,11 @@
<string name="website">Website</string>
<!-- Settings -->
<string name="user_interface">User interface</string>
<string name="user_interface">Content &amp; User interface</string>
<string name="other">Other</string>
<string name="enable_dark_mode">Enable dark theme</string>
<string name="show_url_suggestions">Show URL suggestsions</string>
<string name="show_mature_content">Show mature content</string>
<string name="show_url_suggestions">Show URL suggestions</string>
<string name="notifications">Notifications</string>
<string name="subscriptions">Subscriptions</string>
<string name="content_interests">Content Interests</string>
@ -172,6 +162,7 @@
</plurals>
<!-- Dialogs -->
<string name="customize_your_tags">Customize your tags</string>
<string name="sort_content_by">Sort content by</string>
<string name="content_from">Content from</string>
<string name="trending_content">Trending content</string>
@ -187,9 +178,16 @@
<string name="all_time">All time</string>
<string name="from">from</string>
<string name="for_text">for</string>
<string name="filter_for">Filter for</string>
<string name="everyone">Everyone</string>
<string name="tags_you_follow">Tags you follow</string>
<string name="customize">Customize</string>
<string name="not_yet_implemented">The selected view is not yet available.</string>
<string name="customize_tags_hint">It looks like you have not followed any tags yet.</string>
<string name="search_for_more_tags">Search for more tags</string>
<string name="no_followed_tags">You have not followed any tags yet. Get started by adding tags that you are interested in!</string>
<string name="no_tag_results">We could not find new tags that you\'re not following.</string>
<string name="tag_already_followed">The \'%1$s\' tag has already been added.</string>
<!-- Verification -->
<string name="provide_email_address">Please provide an email address.</string>

View file

@ -10,6 +10,10 @@
app:key="io.lbry.browser.preference.userinterface.DarkMode"
app:title="@string/enable_dark_mode"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="io.lbry.browser.preference.userinterface.ShowMatureContent"
app:title="@string/show_mature_content"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="io.lbry.browser.preference.userinterface.UrlSuggestions"
app:title="@string/show_url_suggestions"
@ -23,14 +27,17 @@
<SwitchPreferenceCompat
app:key="io.lbry.browser.preference.notifications.Subscriptions"
app:title="@string/subscriptions"
app:defaultValue="true"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="io.lbry.browser.preference.notifications.Rewards"
app:title="@string/rewards"
app:defaultValue="true"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="io.lbry.browser.preference.notifications.ContentInterests"
app:title="@string/content_interests"
app:defaultValue="true"
app:iconSpaceReserved="false" />
</PreferenceCategory>