In-app notifications #969
15 changed files with 296 additions and 112 deletions
|
@ -16,8 +16,8 @@ android {
|
||||||
applicationId "io.lbry.browser"
|
applicationId "io.lbry.browser"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 1514
|
versionCode 1515
|
||||||
versionName "0.15.14"
|
versionName "0.15.15"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
@ -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.76.0'
|
__32bitImplementation 'io.lbry:lbrysdk32:0.79.1'
|
||||||
__64bitImplementation 'io.lbry:lbrysdk64:0.76.0'
|
__64bitImplementation 'io.lbry:lbrysdk64:0.79.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
|
|
@ -56,6 +56,13 @@
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="lbry" />
|
<data android:scheme="lbry" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" android:host="open.lbry.com"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|
BIN
app/src/main/assets/font_awesome_5_free_regular.otf
Normal file
BIN
app/src/main/assets/font_awesome_5_free_regular.otf
Normal file
Binary file not shown.
|
@ -306,7 +306,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
public static final String SECURE_VALUE_KEY_SAVED_PASSWORD = "io.lbry.browser.PX";
|
public static final String SECURE_VALUE_KEY_SAVED_PASSWORD = "io.lbry.browser.PX";
|
||||||
public static final String SECURE_VALUE_FIRST_RUN_PASSWORD = "firstRunPassword";
|
public static final String SECURE_VALUE_FIRST_RUN_PASSWORD = "firstRunPassword";
|
||||||
|
|
||||||
private static final String TAG = "io.lbry.browser.Main";
|
private static final String TAG = "LbryMain";
|
||||||
|
|
||||||
private NavigationMenuAdapter navMenuAdapter;
|
private NavigationMenuAdapter navMenuAdapter;
|
||||||
private UrlSuggestionListAdapter urlSuggestionListAdapter;
|
private UrlSuggestionListAdapter urlSuggestionListAdapter;
|
||||||
|
@ -1585,7 +1585,6 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
|
|
||||||
findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
|
findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
|
||||||
|
|
||||||
syncWalletAndLoadPreferences();
|
|
||||||
scheduleWalletBalanceUpdate();
|
scheduleWalletBalanceUpdate();
|
||||||
scheduleWalletSyncTask();
|
scheduleWalletSyncTask();
|
||||||
fetchOwnChannels();
|
fetchOwnChannels();
|
||||||
|
@ -1807,7 +1806,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
//openNavFragments.get
|
//openNavFragments.get
|
||||||
MergeSubscriptionsTask mergeTask = new MergeSubscriptionsTask(
|
MergeSubscriptionsTask mergeTask = new MergeSubscriptionsTask(
|
||||||
subscriptions,
|
subscriptions,
|
||||||
!initialSubscriptionMergeDone(),
|
initialSubscriptionMergeDone(),
|
||||||
MainActivity.this, new MergeSubscriptionsTask.MergeSubscriptionsHandler() {
|
MainActivity.this, new MergeSubscriptionsTask.MergeSubscriptionsHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Subscription> subscriptions, List<Subscription> diff) {
|
public void onSuccess(List<Subscription> subscriptions, List<Subscription> diff) {
|
||||||
|
@ -1818,11 +1817,13 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
|
|
||||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
|
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
|
||||||
sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_INITIAL_SUBSCRIPTION_MERGE_DONE, true).apply();
|
sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_INITIAL_SUBSCRIPTION_MERGE_DONE, true).apply();
|
||||||
|
Lbryio.cacheResolvedSubscriptions.clear();
|
||||||
|
|
||||||
for (Fragment fragment : openNavFragments.values()) {
|
for (Fragment fragment : openNavFragments.values()) {
|
||||||
if (fragment instanceof FollowingFragment) {
|
if (fragment instanceof FollowingFragment) {
|
||||||
// reload local subscriptions
|
// reload local subscriptions
|
||||||
((FollowingFragment) fragment).fetchLoadedSubscriptions(true);
|
FollowingFragment followingFragment = (FollowingFragment) fragment;
|
||||||
|
followingFragment.fetchLoadedSubscriptions(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1960,7 +1961,6 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
public void onSyncGetSuccess(WalletSync walletSync) {
|
public void onSyncGetSuccess(WalletSync walletSync) {
|
||||||
Lbryio.lastWalletSync = walletSync;
|
Lbryio.lastWalletSync = walletSync;
|
||||||
Lbryio.lastRemoteHash = walletSync.getHash();
|
Lbryio.lastRemoteHash = walletSync.getHash();
|
||||||
loadSharedUserState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -2678,7 +2678,6 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
LbryAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_OPEN, bundle);
|
LbryAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_OPEN, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void registerServiceActionsReceiver() {
|
private void registerServiceActionsReceiver() {
|
||||||
IntentFilter intentFilter = new IntentFilter();
|
IntentFilter intentFilter = new IntentFilter();
|
||||||
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
|
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
|
||||||
|
@ -2840,9 +2839,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
JSONObject startupStatus = status.getJSONObject("startup_status");
|
JSONObject startupStatus = status.getJSONObject("startup_status");
|
||||||
sdkReady = startupStatus.getBoolean("file_manager") && startupStatus.getBoolean("wallet");
|
sdkReady = startupStatus.getBoolean("file_manager") && startupStatus.getBoolean("wallet");
|
||||||
}
|
}
|
||||||
} catch (ConnectException ex) {
|
} catch (ConnectException | JSONException ex) {
|
||||||
// pass
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
// pass
|
// pass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
65
app/src/main/java/io/lbry/browser/tasks/BufferEventTask.java
Normal file
65
app/src/main/java/io/lbry/browser/tasks/BufferEventTask.java
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package io.lbry.browser.tasks;
|
||||||
|
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.lbry.browser.utils.Helper;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class BufferEventTask extends AsyncTask<Void, Void, Void> {
|
||||||
|
private static final String TAG = "LbryBufferEvent";
|
||||||
|
private static final String ENDPOINT = "https://collector-service.api.lbry.tv/api/v1/events/video";
|
||||||
|
|
||||||
|
private String streamUrl;
|
||||||
|
private String userIdHash;
|
||||||
|
private long streamDuration;
|
||||||
|
private long streamPosition;
|
||||||
|
private long bufferDuration;
|
||||||
|
|
||||||
|
public BufferEventTask(String streamUrl, long streamDuration, long streamPosition, long bufferDuration, String userIdHash) {
|
||||||
|
this.streamUrl = streamUrl;
|
||||||
|
this.bufferDuration = bufferDuration;
|
||||||
|
this.streamDuration = streamDuration;
|
||||||
|
this.streamPosition = streamPosition;
|
||||||
|
this.userIdHash = userIdHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Void doInBackground(Void... params) {
|
||||||
|
JSONObject requestBody = new JSONObject();
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
try {
|
||||||
|
data.put("url", streamUrl);
|
||||||
|
data.put("position", streamPosition);
|
||||||
|
data.put("stream_duration", streamDuration);
|
||||||
|
//data.put("duration", bufferDuration);
|
||||||
|
|
||||||
|
requestBody.put("device", "android");
|
||||||
|
requestBody.put("type", "buffering");
|
||||||
|
requestBody.put("client", userIdHash);
|
||||||
|
requestBody.put("data", data);
|
||||||
|
|
||||||
|
RequestBody body = RequestBody.create(requestBody.toString(), Helper.JSON_MEDIA_TYPE);
|
||||||
|
Request request = new Request.Builder().url(ENDPOINT).post(body).build();
|
||||||
|
OkHttpClient client = new OkHttpClient.Builder().
|
||||||
|
writeTimeout(60, TimeUnit.SECONDS).
|
||||||
|
readTimeout(60, TimeUnit.SECONDS).
|
||||||
|
build();
|
||||||
|
|
||||||
|
Response response = client.newCall(request).execute();
|
||||||
|
String responseString = response.body().string();
|
||||||
|
Log.d(TAG, String.format("buffer event sent: %s", responseString));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// we don't want to fail if a buffer event fails to register
|
||||||
|
Log.d(TAG, String.format("buffer event log failed: %s", ex.getMessage()), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,6 +73,7 @@ public class MergeSubscriptionsTask extends AsyncTask<Void, Void, List<Subscript
|
||||||
// fetch remote subscriptions
|
// fetch remote subscriptions
|
||||||
JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
|
JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
|
||||||
if (array != null) {
|
if (array != null) {
|
||||||
|
// check for any remote subs that may have been removed, and unsubscribe from them
|
||||||
for (int i = 0; i < array.length(); i++) {
|
for (int i = 0; i < array.length(); i++) {
|
||||||
JSONObject item = array.getJSONObject(i);
|
JSONObject item = array.getJSONObject(i);
|
||||||
String claimId = item.getString("claim_id");
|
String claimId = item.getString("claim_id");
|
||||||
|
@ -86,23 +87,21 @@ public class MergeSubscriptionsTask extends AsyncTask<Void, Void, List<Subscript
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < combined.size(); i++) {
|
List<Subscription> remoteUnsubs = new ArrayList<>();
|
||||||
Subscription local = combined.get(i);
|
List<Subscription> finalRemoteSubs = new ArrayList<>();
|
||||||
if (!remoteSubs.contains(local)) {
|
if (remoteSubs.size() > 0) {
|
||||||
// add to remote subscriptions
|
for (int i = 0; i < remoteSubs.size(); i++) {
|
||||||
try {
|
Subscription sub = remoteSubs.get(i);
|
||||||
LbryUri uri = LbryUri.parse(local.getUrl());
|
if (!combined.contains(sub)) {
|
||||||
Map<String, String> options = new HashMap<>();
|
Map<String, String> options = new HashMap<>();
|
||||||
String channelClaimId = uri.getChannelClaimId();
|
LbryUri uri = LbryUri.tryParse(sub.getUrl());
|
||||||
String channelName = Helper.normalizeChannelName(local.getChannelName());
|
if (uri != null) {
|
||||||
if (!Helper.isNullOrEmpty(channelClaimId) && !Helper.isNullOrEmpty(channelName)) {
|
options.put("claim_id", uri.getChannelClaimId());
|
||||||
options.put("claim_id", channelClaimId);
|
Lbryio.parseResponse(Lbryio.call("subscription", "delete", options, context));
|
||||||
options.put("channel_name", channelName);
|
remoteUnsubs.add(sub);
|
||||||
Lbryio.parseResponse(Lbryio.call("subscription", "new", options, context));
|
} else {
|
||||||
|
finalRemoteSubs.add(sub);
|
||||||
}
|
}
|
||||||
} catch (LbryUriException | LbryioRequestException | LbryioResponseException ex) {
|
|
||||||
// pass
|
|
||||||
Log.e(TAG, String.format("subscription/new failed: %s", ex.getMessage()), ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,13 +113,13 @@ public class MergeSubscriptionsTask extends AsyncTask<Void, Void, List<Subscript
|
||||||
diff.add(local);
|
diff.add(local);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (int i = 0; i < finalRemoteSubs.size(); i++) {
|
||||||
for (int i = 0; i < remoteSubs.size(); i++) {
|
Subscription remote = finalRemoteSubs.get(i);
|
||||||
Subscription remote = remoteSubs.get(i);
|
if (!combined.contains(remote)) {
|
||||||
if (!combined.contains(remote)) {
|
combined.add(remote);
|
||||||
combined.add(remote);
|
if (!diff.contains(remote)) {
|
||||||
if (!diff.contains(remote)) {
|
diff.add(remote);
|
||||||
diff.add(remote);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ public class SyncGetTask extends AsyncTask<Void, Void, WalletSync> {
|
||||||
if (applySyncChanges) {
|
if (applySyncChanges) {
|
||||||
if (applySyncSuccessful) {
|
if (applySyncSuccessful) {
|
||||||
handler.onSyncApplySuccess(syncHash, syncData);
|
handler.onSyncApplySuccess(syncHash, syncData);
|
||||||
} else {
|
} else if (syncApplyError != null) {
|
||||||
handler.onSyncApplyError(syncApplyError);
|
handler.onSyncApplyError(syncApplyError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ import io.lbry.browser.tasks.claim.ClaimListResultHandler;
|
||||||
import io.lbry.browser.tasks.claim.ResolveTask;
|
import io.lbry.browser.tasks.claim.ResolveTask;
|
||||||
import io.lbry.browser.tasks.lbryinc.FetchStatCountTask;
|
import io.lbry.browser.tasks.lbryinc.FetchStatCountTask;
|
||||||
import io.lbry.browser.ui.BaseFragment;
|
import io.lbry.browser.ui.BaseFragment;
|
||||||
|
import io.lbry.browser.ui.controls.OutlineIconView;
|
||||||
import io.lbry.browser.ui.controls.SolidIconView;
|
import io.lbry.browser.ui.controls.SolidIconView;
|
||||||
import io.lbry.browser.ui.findcontent.FollowingFragment;
|
import io.lbry.browser.ui.findcontent.FollowingFragment;
|
||||||
import io.lbry.browser.utils.Helper;
|
import io.lbry.browser.utils.Helper;
|
||||||
|
@ -80,7 +81,8 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen
|
||||||
private View buttonTip;
|
private View buttonTip;
|
||||||
private View buttonFollowUnfollow;
|
private View buttonFollowUnfollow;
|
||||||
private int subCount;
|
private int subCount;
|
||||||
private SolidIconView iconFollowUnfollow;
|
private OutlineIconView iconFollow;
|
||||||
|
private SolidIconView iconUnfollow;
|
||||||
private View layoutNothingAtLocation;
|
private View layoutNothingAtLocation;
|
||||||
private View layoutLoadingState;
|
private View layoutLoadingState;
|
||||||
|
|
||||||
|
@ -105,7 +107,8 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen
|
||||||
buttonShare = root.findViewById(R.id.channel_view_share);
|
buttonShare = root.findViewById(R.id.channel_view_share);
|
||||||
buttonTip = root.findViewById(R.id.channel_view_tip);
|
buttonTip = root.findViewById(R.id.channel_view_tip);
|
||||||
buttonFollowUnfollow = root.findViewById(R.id.channel_view_follow_unfollow);
|
buttonFollowUnfollow = root.findViewById(R.id.channel_view_follow_unfollow);
|
||||||
iconFollowUnfollow = root.findViewById(R.id.channel_view_icon_follow_unfollow);
|
iconFollow = root.findViewById(R.id.channel_view_icon_follow);
|
||||||
|
iconUnfollow = root.findViewById(R.id.channel_view_icon_unfollow);
|
||||||
|
|
||||||
tabPager = root.findViewById(R.id.channel_view_pager);
|
tabPager = root.findViewById(R.id.channel_view_pager);
|
||||||
tabLayout = root.findViewById(R.id.channel_view_tabs);
|
tabLayout = root.findViewById(R.id.channel_view_tabs);
|
||||||
|
@ -274,13 +277,8 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen
|
||||||
private void checkIsFollowing() {
|
private void checkIsFollowing() {
|
||||||
if (claim != null) {
|
if (claim != null) {
|
||||||
boolean isFollowing = Lbryio.isFollowing(claim);
|
boolean isFollowing = Lbryio.isFollowing(claim);
|
||||||
if (iconFollowUnfollow != null) {
|
Helper.setViewVisibility(iconFollow, !isFollowing ? View.VISIBLE : View.GONE);
|
||||||
iconFollowUnfollow.setText(isFollowing ? R.string.fa_heart_broken : R.string.fa_heart);
|
Helper.setViewVisibility(iconUnfollow, isFollowing ? View.VISIBLE : View.GONE);
|
||||||
Context context = getContext();
|
|
||||||
if (context != null) {
|
|
||||||
iconFollowUnfollow.setTextColor(ContextCompat.getColor(context, isFollowing ? R.color.foreground : R.color.red));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package io.lbry.browser.ui.controls;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.Gravity;
|
||||||
|
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView;
|
||||||
|
|
||||||
|
public class OutlineIconView extends AppCompatTextView {
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public OutlineIconView(Context context) {
|
||||||
|
super(context);
|
||||||
|
this.context = context;
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutlineIconView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
this.context = context;
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
setGravity(Gravity.CENTER);
|
||||||
|
setTypeface(Typeface.createFromAsset(context.getAssets(), "font_awesome_5_free_regular.otf"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Outline;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -119,6 +120,7 @@ import io.lbry.browser.model.UrlSuggestion;
|
||||||
import io.lbry.browser.model.WalletBalance;
|
import io.lbry.browser.model.WalletBalance;
|
||||||
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.BufferEventTask;
|
||||||
import io.lbry.browser.tasks.CommentCreateWithTipTask;
|
import io.lbry.browser.tasks.CommentCreateWithTipTask;
|
||||||
import io.lbry.browser.tasks.CommentListHandler;
|
import io.lbry.browser.tasks.CommentListHandler;
|
||||||
import io.lbry.browser.tasks.CommentListTask;
|
import io.lbry.browser.tasks.CommentListTask;
|
||||||
|
@ -141,6 +143,7 @@ import io.lbry.browser.tasks.lbryinc.ClaimRewardTask;
|
||||||
import io.lbry.browser.tasks.lbryinc.FetchStatCountTask;
|
import io.lbry.browser.tasks.lbryinc.FetchStatCountTask;
|
||||||
import io.lbry.browser.tasks.lbryinc.LogFileViewTask;
|
import io.lbry.browser.tasks.lbryinc.LogFileViewTask;
|
||||||
import io.lbry.browser.ui.BaseFragment;
|
import io.lbry.browser.ui.BaseFragment;
|
||||||
|
import io.lbry.browser.ui.controls.OutlineIconView;
|
||||||
import io.lbry.browser.ui.controls.SolidIconView;
|
import io.lbry.browser.ui.controls.SolidIconView;
|
||||||
import io.lbry.browser.ui.publish.PublishFragment;
|
import io.lbry.browser.ui.publish.PublishFragment;
|
||||||
import io.lbry.browser.utils.Helper;
|
import io.lbry.browser.utils.Helper;
|
||||||
|
@ -163,6 +166,7 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
WalletBalanceListener {
|
WalletBalanceListener {
|
||||||
private static final int RELATED_CONTENT_SIZE = 16;
|
private static final int RELATED_CONTENT_SIZE = 16;
|
||||||
private static final String DEFAULT_PLAYBACK_SPEED = "1x";
|
private static final String DEFAULT_PLAYBACK_SPEED = "1x";
|
||||||
|
private static final String CDN_PREFIX = "https://cdn.lbryplayer.xyz";
|
||||||
|
|
||||||
private PlayerControlView castControlView;
|
private PlayerControlView castControlView;
|
||||||
private Player currentPlayer;
|
private Player currentPlayer;
|
||||||
|
@ -290,6 +294,27 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
loadingNewClaim = false;
|
loadingNewClaim = false;
|
||||||
}
|
}
|
||||||
} else if (playbackState == Player.STATE_BUFFERING) {
|
} else if (playbackState == Player.STATE_BUFFERING) {
|
||||||
|
if (MainActivity.appPlayer != null && MainActivity.appPlayer.getCurrentPosition() > 0) {
|
||||||
|
// we only want to log a buffer event after the media has already started playing
|
||||||
|
String mediaSourceUrl = getStreamingUrl();
|
||||||
|
long duration = MainActivity.appPlayer.getDuration();
|
||||||
|
long position = MainActivity.appPlayer.getCurrentPosition();
|
||||||
|
// TODO: Determine a hash for the userId
|
||||||
|
String userIdHash = Helper.SHA256(Lbryio.currentUser != null ? String.valueOf(Lbryio.currentUser.getId()) : "0");
|
||||||
|
if (mediaSourceUrl.startsWith(CDN_PREFIX)) {
|
||||||
|
BufferEventTask bufferEvent = new BufferEventTask(claim.getPermanentUrl(), duration, position, 1, userIdHash);
|
||||||
|
bufferEvent.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||||
|
} else {
|
||||||
|
// sdk stream buffer events should be handled differently
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("url", claim.getPermanentUrl());
|
||||||
|
bundle.putLong("stream_duration", duration);
|
||||||
|
bundle.putLong("stream_position", position);
|
||||||
|
bundle.putString("user_id_hash", userIdHash);
|
||||||
|
LbryAnalytics.logEvent(LbryAnalytics.EVENT_BUFFER, bundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showBuffering();
|
showBuffering();
|
||||||
} else {
|
} else {
|
||||||
hideBuffering();
|
hideBuffering();
|
||||||
|
@ -724,6 +749,44 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private View.OnClickListener followUnfollowListener = new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
if (claim != null && claim.getSigningChannel() != null) {
|
||||||
|
Claim publisher = claim.getSigningChannel();
|
||||||
|
boolean isFollowing = Lbryio.isFollowing(publisher);
|
||||||
|
Subscription subscription = Subscription.fromClaim(publisher);
|
||||||
|
view.setEnabled(false);
|
||||||
|
Context context = getContext();
|
||||||
|
new ChannelSubscribeTask(context, publisher.getClaimId(), subscription, isFollowing, new ChannelSubscribeTask.ChannelSubscribeHandler() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
if (isFollowing) {
|
||||||
|
Lbryio.removeSubscription(subscription);
|
||||||
|
Lbryio.removeCachedResolvedSubscription(publisher);
|
||||||
|
} else {
|
||||||
|
Lbryio.addSubscription(subscription);
|
||||||
|
Lbryio.addCachedResolvedSubscription(publisher);
|
||||||
|
}
|
||||||
|
view.setEnabled(true);
|
||||||
|
checkIsFollowing();
|
||||||
|
FollowingFragment.resetClaimSearchContent = true;
|
||||||
|
|
||||||
|
// Save shared user state
|
||||||
|
if (context != null) {
|
||||||
|
context.sendBroadcast(new Intent(MainActivity.ACTION_SAVE_SHARED_USER_STATE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception exception) {
|
||||||
|
view.setEnabled(true);
|
||||||
|
}
|
||||||
|
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private void resolveUrl(String url) {
|
private void resolveUrl(String url) {
|
||||||
resolving = true;
|
resolving = true;
|
||||||
Helper.setViewVisibility(layoutDisplayArea, View.INVISIBLE);
|
Helper.setViewVisibility(layoutDisplayArea, View.INVISIBLE);
|
||||||
|
@ -1060,44 +1123,10 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
View buttonFollowUnfollow = root.findViewById(R.id.file_view_icon_follow_unfollow);
|
View buttonFollow = root.findViewById(R.id.file_view_icon_follow);
|
||||||
buttonFollowUnfollow.setOnClickListener(new View.OnClickListener() {
|
View buttonUnfollow = root.findViewById(R.id.file_view_icon_unfollow);
|
||||||
@Override
|
buttonFollow.setOnClickListener(followUnfollowListener);
|
||||||
public void onClick(View view) {
|
buttonUnfollow.setOnClickListener(followUnfollowListener);
|
||||||
if (claim != null && claim.getSigningChannel() != null) {
|
|
||||||
Claim publisher = claim.getSigningChannel();
|
|
||||||
boolean isFollowing = Lbryio.isFollowing(publisher);
|
|
||||||
Subscription subscription = Subscription.fromClaim(publisher);
|
|
||||||
buttonFollowUnfollow.setEnabled(false);
|
|
||||||
Context context = getContext();
|
|
||||||
new ChannelSubscribeTask(context, publisher.getClaimId(), subscription, isFollowing, new ChannelSubscribeTask.ChannelSubscribeHandler() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess() {
|
|
||||||
if (isFollowing) {
|
|
||||||
Lbryio.removeSubscription(subscription);
|
|
||||||
Lbryio.removeCachedResolvedSubscription(publisher);
|
|
||||||
} else {
|
|
||||||
Lbryio.addSubscription(subscription);
|
|
||||||
Lbryio.addCachedResolvedSubscription(publisher);
|
|
||||||
}
|
|
||||||
buttonFollowUnfollow.setEnabled(true);
|
|
||||||
checkIsFollowing();
|
|
||||||
FollowingFragment.resetClaimSearchContent = true;
|
|
||||||
|
|
||||||
// Save shared user state
|
|
||||||
if (context != null) {
|
|
||||||
context.sendBroadcast(new Intent(MainActivity.ACTION_SAVE_SHARED_USER_STATE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Exception exception) {
|
|
||||||
buttonFollowUnfollow.setEnabled(true);
|
|
||||||
}
|
|
||||||
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
commentChannelSpinnerAdapter = new InlineChannelSpinnerAdapter(getContext(), R.layout.spinner_item_channel, new ArrayList<>());
|
commentChannelSpinnerAdapter = new InlineChannelSpinnerAdapter(getContext(), R.layout.spinner_item_channel, new ArrayList<>());
|
||||||
commentChannelSpinnerAdapter.addPlaceholder(false);
|
commentChannelSpinnerAdapter.addPlaceholder(false);
|
||||||
|
@ -1418,7 +1447,17 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.findViewById(R.id.file_view_icon_follow_unfollow).setVisibility(claim.getSigningChannel() != null ? View.VISIBLE : View.GONE);
|
boolean isAnonymous = claim.getSigningChannel() == null;
|
||||||
|
View iconFollow = root.findViewById(R.id.file_view_icon_follow);
|
||||||
|
View iconUnfollow = root.findViewById(R.id.file_view_icon_unfollow);
|
||||||
|
if (isAnonymous) {
|
||||||
|
if (iconFollow.getVisibility() == View.VISIBLE) {
|
||||||
|
iconFollow.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
if (iconUnfollow.getVisibility() == View.VISIBLE) {
|
||||||
|
iconUnfollow.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MaterialButton mainActionButton = root.findViewById(R.id.file_view_main_action_button);
|
MaterialButton mainActionButton = root.findViewById(R.id.file_view_main_action_button);
|
||||||
if (claim.isPlayable()) {
|
if (claim.isPlayable()) {
|
||||||
|
@ -2408,11 +2447,10 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
View root = getView();
|
View root = getView();
|
||||||
if (context != null && root != null) {
|
if (context != null && root != null) {
|
||||||
SolidIconView iconFollowUnfollow = root.findViewById(R.id.file_view_icon_follow_unfollow);
|
OutlineIconView iconFollow = root.findViewById(R.id.file_view_icon_follow);
|
||||||
if (iconFollowUnfollow != null) {
|
SolidIconView iconUnfollow = root.findViewById(R.id.file_view_icon_unfollow);
|
||||||
iconFollowUnfollow.setText(isFollowing ? R.string.fa_heart_broken : R.string.fa_heart);
|
Helper.setViewVisibility(iconFollow, !isFollowing ? View.VISIBLE: View.INVISIBLE);
|
||||||
iconFollowUnfollow.setTextColor(ContextCompat.getColor(context, isFollowing ? R.color.foreground : R.color.red));
|
Helper.setViewVisibility(iconUnfollow, isFollowing ? View.VISIBLE : View.INVISIBLE);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.util.Hex;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
@ -37,6 +39,9 @@ import org.json.JSONObject;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -766,4 +771,14 @@ public final class Helper {
|
||||||
}
|
}
|
||||||
return id.toString();
|
return id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String SHA256(String value) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] hash = digest.digest(value.getBytes("UTF-8"));
|
||||||
|
return Hex.bytesToStringLowercase(hash);
|
||||||
|
} catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ public class LbryAnalytics {
|
||||||
public static final String EVENT_APP_ERROR = "app_error";
|
public static final String EVENT_APP_ERROR = "app_error";
|
||||||
public static final String EVENT_APP_LAUNCH = "app_launch";
|
public static final String EVENT_APP_LAUNCH = "app_launch";
|
||||||
public static final String EVENT_COMMENT_CREATE = "comment_create";
|
public static final String EVENT_COMMENT_CREATE = "comment_create";
|
||||||
|
public static final String EVENT_BUFFER = "buffer";
|
||||||
public static final String EVENT_EMAIL_ADDED = "email_added";
|
public static final String EVENT_EMAIL_ADDED = "email_added";
|
||||||
public static final String EVENT_EMAIL_VERIFIED = "email_verified";
|
public static final String EVENT_EMAIL_VERIFIED = "email_verified";
|
||||||
public static final String EVENT_FIRST_RUN_COMPLETED = "first_run_completed";
|
public static final String EVENT_FIRST_RUN_COMPLETED = "first_run_completed";
|
||||||
|
|
|
@ -17,7 +17,8 @@ public class LbryUri {
|
||||||
public static final int CHANNEL_NAME_MIN_LENGTH = 1;
|
public static final int CHANNEL_NAME_MIN_LENGTH = 1;
|
||||||
public static final int CLAIM_ID_MAX_LENGTH = 40;
|
public static final int CLAIM_ID_MAX_LENGTH = 40;
|
||||||
|
|
||||||
private static final String REGEX_PART_PROTOCOL = "^((?:lbry://)?)";
|
private static final String REGEX_PART_PROTOCOL = "^((?:lbry://|https://)?)";
|
||||||
|
private static final String REGEX_PART_HOST = "((?:open.lbry.com/)?)";
|
||||||
private static final String REGEX_PART_STREAM_OR_CHANNEL_NAME = "([^:$#/]*)";
|
private static final String REGEX_PART_STREAM_OR_CHANNEL_NAME = "([^:$#/]*)";
|
||||||
private static final String REGEX_PART_MODIFIER_SEPARATOR = "([:$#]?)([^/]*)";
|
private static final String REGEX_PART_MODIFIER_SEPARATOR = "([:$#]?)([^/]*)";
|
||||||
private static final String QUERY_STRING_BREAKER = "^([\\S]+)([?][\\S]*)";
|
private static final String QUERY_STRING_BREAKER = "^([\\S]+)([?][\\S]*)";
|
||||||
|
@ -58,8 +59,9 @@ public class LbryUri {
|
||||||
return parse(url, false);
|
return parse(url, false);
|
||||||
}
|
}
|
||||||
public static LbryUri parse(String url, boolean requireProto) throws LbryUriException {
|
public static LbryUri parse(String url, boolean requireProto) throws LbryUriException {
|
||||||
Pattern componentsPattern = Pattern.compile(String.format("%s%s%s(/?)%s%s",
|
Pattern componentsPattern = Pattern.compile(String.format("%s%s%s%s(/?)%s%s",
|
||||||
REGEX_PART_PROTOCOL,
|
REGEX_PART_PROTOCOL,
|
||||||
|
REGEX_PART_HOST,
|
||||||
REGEX_PART_STREAM_OR_CHANNEL_NAME,
|
REGEX_PART_STREAM_OR_CHANNEL_NAME,
|
||||||
REGEX_PART_MODIFIER_SEPARATOR,
|
REGEX_PART_MODIFIER_SEPARATOR,
|
||||||
REGEX_PART_STREAM_OR_CHANNEL_NAME,
|
REGEX_PART_STREAM_OR_CHANNEL_NAME,
|
||||||
|
@ -93,37 +95,48 @@ public class LbryUri {
|
||||||
}
|
}
|
||||||
|
|
||||||
// components[0] = proto
|
// components[0] = proto
|
||||||
// components[1] = streamNameOrChannelName
|
// components[1] = host
|
||||||
// components[2] = primaryModSeparator
|
// components[2] = streamNameOrChannelName
|
||||||
// components[3] = primaryModValue
|
// components[3] = primaryModSeparator
|
||||||
// components[4] = pathSep
|
// components[4] = primaryModValue
|
||||||
// components[5] = possibleStreamName
|
// components[5] = pathSep
|
||||||
// components[6] = secondaryModSeparator
|
// components[6] = possibleStreamName
|
||||||
// components[7] = secondaryModValue
|
// components[7] = secondaryModSeparator
|
||||||
|
// components[8] = secondaryModValue
|
||||||
if (requireProto && Helper.isNullOrEmpty(components.get(0))) {
|
if (requireProto && Helper.isNullOrEmpty(components.get(0))) {
|
||||||
throw new LbryUriException("LBRY URLs must include a protocol prefix (lbry://).");
|
throw new LbryUriException("LBRY URLs must include a protocol prefix (lbry://).");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Helper.isNullOrEmpty(components.get(1))) {
|
if (Helper.isNullOrEmpty(components.get(2))) {
|
||||||
throw new LbryUriException("URL does not include name.");
|
throw new LbryUriException("URL does not include name.");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String component : components.subList(1, components.size())) {
|
for (String component : components.subList(2, components.size())) {
|
||||||
if (component.indexOf(' ') > -1) {
|
if (component.indexOf(' ') > -1) {
|
||||||
throw new LbryUriException("URL cannot include a space.");
|
throw new LbryUriException("URL cannot include a space.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String streamOrChannelName = components.get(1);
|
String streamOrChannelName = components.get(2);
|
||||||
String primaryModSeparator = components.get(2);
|
String primaryModSeparator = components.get(3);
|
||||||
String primaryModValue = components.get(3);
|
String primaryModValue = components.get(4);
|
||||||
String possibleStreamName = components.get(5);
|
String possibleStreamName = components.get(6);
|
||||||
String secondaryModSeparator = components.get(6);
|
String secondaryModSeparator = components.get(7);
|
||||||
String secondaryModValue = components.get(7);
|
String secondaryModValue = components.get(8);
|
||||||
|
|
||||||
boolean includesChannel = streamOrChannelName.startsWith("@");
|
boolean includesChannel = streamOrChannelName.startsWith("@");
|
||||||
boolean isChannel = includesChannel && Helper.isNullOrEmpty(possibleStreamName);
|
boolean isChannel = includesChannel && Helper.isNullOrEmpty(possibleStreamName);
|
||||||
String channelName = includesChannel && streamOrChannelName.length() > 1 ? streamOrChannelName.substring(1) : null;
|
String channelName = includesChannel && streamOrChannelName.length() > 1 ? streamOrChannelName.substring(1) : null;
|
||||||
|
|
||||||
|
// It would have thrown already on the RegEx parser if protocol value was incorrect
|
||||||
|
// open.lbry.com uses ':' as ModSeparators while lbry:// expects '#'
|
||||||
|
if (components.get(1).equals("open.lbry.com/")) {
|
||||||
|
if (primaryModSeparator.equals(":"))
|
||||||
|
primaryModSeparator = "#";
|
||||||
|
if (secondaryModSeparator.equals(":"))
|
||||||
|
secondaryModSeparator = "#";
|
||||||
|
}
|
||||||
|
|
||||||
if (includesChannel) {
|
if (includesChannel) {
|
||||||
if (Helper.isNullOrEmpty(channelName)) {
|
if (Helper.isNullOrEmpty(channelName)) {
|
||||||
throw new LbryUriException("No channel name after @.");
|
throw new LbryUriException("No channel name after @.");
|
||||||
|
@ -147,7 +160,7 @@ public class LbryUri {
|
||||||
|
|
||||||
LbryUri uri = new LbryUri();
|
LbryUri uri = new LbryUri();
|
||||||
uri.setChannel(isChannel);
|
uri.setChannel(isChannel);
|
||||||
uri.setPath(Helper.join(components.subList(1, components.size()), ""));
|
uri.setPath(Helper.join(components.subList(2, components.size()), ""));
|
||||||
uri.setStreamName(streamName);
|
uri.setStreamName(streamName);
|
||||||
uri.setStreamClaimId(streamClaimId);
|
uri.setStreamClaimId(streamClaimId);
|
||||||
uri.setChannelName(channelName);
|
uri.setChannelName(channelName);
|
||||||
|
|
|
@ -210,14 +210,22 @@
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:layout_width="36dp"
|
android:layout_width="36dp"
|
||||||
android:layout_height="36dp">
|
android:layout_height="36dp">
|
||||||
<io.lbry.browser.ui.controls.SolidIconView
|
<io.lbry.browser.ui.controls.OutlineIconView
|
||||||
android:id="@+id/channel_view_icon_follow_unfollow"
|
android:id="@+id/channel_view_icon_follow"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:text="@string/fa_heart"
|
android:text="@string/fa_heart"
|
||||||
android:textColor="@color/red"
|
android:textColor="@color/red"
|
||||||
android:textSize="20dp" />
|
android:textSize="20dp" />
|
||||||
|
<io.lbry.browser.ui.controls.SolidIconView
|
||||||
|
android:id="@+id/channel_view_icon_unfollow"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:text="@string/fa_heart_broken"
|
||||||
|
android:textSize="20dp"
|
||||||
|
android:visibility="invisible"/>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -484,7 +484,7 @@
|
||||||
android:paddingTop="12dp"
|
android:paddingTop="12dp"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
android:paddingBottom="12dp"
|
android:paddingBottom="12dp"
|
||||||
android:layout_toLeftOf="@id/file_view_icon_follow_unfollow">
|
android:layout_toLeftOf="@id/file_view_icon_follow">
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/file_view_publisher_avatar"
|
android:id="@+id/file_view_publisher_avatar"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
|
@ -541,8 +541,8 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<io.lbry.browser.ui.controls.SolidIconView
|
<io.lbry.browser.ui.controls.OutlineIconView
|
||||||
android:id="@+id/file_view_icon_follow_unfollow"
|
android:id="@+id/file_view_icon_follow"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:layout_alignParentRight="true"
|
android:layout_alignParentRight="true"
|
||||||
|
@ -554,6 +554,20 @@
|
||||||
android:text="@string/fa_heart"
|
android:text="@string/fa_heart"
|
||||||
android:textColor="@color/red"
|
android:textColor="@color/red"
|
||||||
android:textSize="20dp" />
|
android:textSize="20dp" />
|
||||||
|
|
||||||
|
<io.lbry.browser.ui.controls.SolidIconView
|
||||||
|
android:id="@+id/file_view_icon_unfollow"
|
||||||
|
android:clickable="true"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:text="@string/fa_heart_broken"
|
||||||
|
android:textSize="20dp"
|
||||||
|
android:visibility="invisible" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|
Loading…
Reference in a new issue