From a0c21c44cb5ea3a1a9d5c9dbe1cf9c924998865b Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Sun, 31 May 2020 15:22:34 +0100 Subject: [PATCH 1/3] implement commenting with tips --- .../browser/adapter/CommentListAdapter.java | 85 +++- .../java/io/lbry/browser/model/Comment.java | 43 +- .../tasks/CommentCreateWithTipTask.java | 103 +++++ .../browser/tasks/CommentListHandler.java | 2 +- .../lbry/browser/tasks/CommentListTask.java | 26 +- .../tasks/wallet/SupportCreateTask.java | 1 + .../java/io/lbry/browser/ui/BaseFragment.java | 145 +++++++ .../ui/channel/ChannelFormFragment.java | 7 - .../ui/findcontent/FileViewFragment.java | 404 +++++++++++++++++- .../ui/publish/PublishFormFragment.java | 128 +----- .../browser/ui/wallet/InvitesFragment.java | 128 +----- .../java/io/lbry/browser/utils/Helper.java | 1 + .../main/java/io/lbry/browser/utils/Lbry.java | 2 + .../res/layout/container_comment_form.xml | 118 +++++ .../main/res/layout/fragment_file_view.xml | 18 +- app/src/main/res/layout/list_item_comment.xml | 86 +++- app/src/main/res/values/strings.xml | 18 +- 17 files changed, 998 insertions(+), 317 deletions(-) create mode 100644 app/src/main/java/io/lbry/browser/tasks/CommentCreateWithTipTask.java create mode 100644 app/src/main/res/layout/container_comment_form.xml diff --git a/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java index 128b6a54..58728ca9 100644 --- a/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java @@ -1,24 +1,46 @@ package io.lbry.browser.adapter; import android.content.Context; +import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; +import java.util.ArrayList; import java.util.List; import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + import io.lbry.browser.R; +import io.lbry.browser.model.Claim; +import io.lbry.browser.model.ClaimCacheKey; import io.lbry.browser.model.Comment; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; +import io.lbry.browser.utils.LbryUri; +import lombok.Setter; public class CommentListAdapter extends RecyclerView.Adapter { private List items; private Context context; + @Setter + private ClaimListAdapter.ClaimListItemListener listener; public CommentListAdapter(List items, Context context) { - this.items = items; + this.items = new ArrayList<>(items); this.context = context; + for (Comment item : this.items) { + ClaimCacheKey key = new ClaimCacheKey(); + key.setClaimId(item.getChannelId()); + if (Lbry.claimCache.containsKey(key)) { + item.setPoster(Lbry.claimCache.get(key)); + } + } } @Override @@ -26,14 +48,53 @@ public class CommentListAdapter extends RecyclerView.Adapter getClaimUrlsToResolve() { + List urls = new ArrayList<>(); + for (int i = 0; i < items.size(); i++) { + Comment item = items.get(i); + if (item.getPoster() == null) { + LbryUri url = LbryUri.tryParse(String.format("%s#%s", item.getChannelName(), item.getChannelId())); + if (url != null) { + urls.add(url.toString()); + } + } + } + return urls; + } + public static class ViewHolder extends RecyclerView.ViewHolder { protected TextView channelName; protected TextView commentText; + protected ImageView thumbnailView; + protected View noThumbnailView; + protected TextView alphaView; + protected TextView commentTimeView; public ViewHolder (View v) { super(v); channelName = v.findViewById(R.id.comment_channel_name); + commentTimeView = v.findViewById(R.id.comment_time); commentText = v.findViewById(R.id.comment_text); + thumbnailView = v.findViewById(R.id.comment_thumbnail); + noThumbnailView = v.findViewById(R.id.comment_no_thumbnail); + alphaView = v.findViewById(R.id.comment_thumbnail_alpha); + } + } + + public void insert(int index, Comment comment) { + if (!items.contains(comment)) { + items.add(index, comment); + notifyDataSetChanged(); + } + } + + public void updatePosterForComment(String channelId, Claim channel) { + for (int i = 0 ; i < items.size(); i++) { + Comment item = items.get(i); + if (channelId.equalsIgnoreCase(item.getChannelId())) { + item.setPoster(channel); + break; + } } } @@ -47,6 +108,28 @@ public class CommentListAdapter extends RecyclerView.Adapter replies; + private long timestamp; + private String channelId; + private String channelName, text, id, parentId; + + public Comment(String channelId, String channelName, String text, String id, String parentId) { + this.channelId = channelId; this.channelName = channelName; this.text = text; this.id = id; this.parentId = parentId; + + this.replies = new ArrayList<>(); + } + + public Comment() { + replies = new ArrayList<>(); + } + + public void addReply(Comment reply) { + if (replies == null) { + replies = new ArrayList<>(); + } + if (!replies.contains(reply)) { + replies.add(reply); + } } public static Comment fromJSONObject(JSONObject jsonObject) { @@ -25,15 +52,17 @@ public class Comment { parentId = jsonObject.getString("parent_id"); } - return new Comment( + Comment comment = new Comment( + Helper.getJSONString("channel_id", null, jsonObject), jsonObject.getString("channel_name"), jsonObject.getString("comment"), jsonObject.getString("comment_id"), parentId ); + comment.setClaimId(Helper.getJSONString("claim_id", null, jsonObject)); + comment.setTimestamp(Helper.getJSONLong("timestamp", 0, jsonObject)); + return comment; } catch (JSONException ex) { - // TODO: Throw exception - Log.e("Comments", ex.toString()); return null; } } diff --git a/app/src/main/java/io/lbry/browser/tasks/CommentCreateWithTipTask.java b/app/src/main/java/io/lbry/browser/tasks/CommentCreateWithTipTask.java new file mode 100644 index 00000000..05e2fca1 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/CommentCreateWithTipTask.java @@ -0,0 +1,103 @@ +package io.lbry.browser.tasks; + +import android.os.AsyncTask; +import android.view.View; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.lbry.browser.exceptions.ApiCallException; +import io.lbry.browser.model.Comment; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; +import io.lbry.browser.utils.Lbryio; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class CommentCreateWithTipTask extends AsyncTask { + private static final String STATUS_ENDPOINT = "https://comments.lbry.com"; + + private Comment comment; + private BigDecimal amount; + private View progressView; + private CommentCreateWithTipHandler handler; + private Exception error; + + public CommentCreateWithTipTask(Comment comment, BigDecimal amount, View progressView, CommentCreateWithTipHandler handler) { + this.comment = comment; + this.amount = amount; + this.progressView = progressView; + this.handler = handler; + } + + protected void onPreExecute() { + Helper.setViewVisibility(progressView, View.VISIBLE); + } + + public Comment doInBackground(Void... params) { + Comment createdComment = null; + try { + // check comments status endpoint + Request request = new Request.Builder().url(STATUS_ENDPOINT).build(); + OkHttpClient client = new OkHttpClient.Builder(). + writeTimeout(30, TimeUnit.SECONDS). + readTimeout(30, TimeUnit.SECONDS). + build(); + Response response = client.newCall(request).execute(); + JSONObject status = new JSONObject(response.body().string()); + String statusText = Helper.getJSONString("text", null, status); + boolean isRunning = Helper.getJSONBoolean("is_running", false, status); + if (!"ok".equalsIgnoreCase(statusText) || !isRunning) { + throw new ApiCallException("The comment server is not available at this time. Please try again later."); + } + + Map options = new HashMap<>(); + options.put("blocking", true); + options.put("claim_id", comment.getClaimId()); + options.put("amount", new DecimalFormat(Helper.SDK_AMOUNT_FORMAT, new DecimalFormatSymbols(Locale.US)).format(amount.doubleValue())); + options.put("tip", true); + Lbry.genericApiCall(Lbry.METHOD_SUPPORT_CREATE, options); + + options = new HashMap<>(); + options.put("comment", comment.getText()); + options.put("claim_id", comment.getClaimId()); + options.put("channel_id", comment.getChannelId()); + options.put("channel_name", comment.getChannelName()); + if (!Helper.isNullOrEmpty(comment.getParentId())) { + options.put("parent_id", comment.getParentId()); + } + JSONObject jsonObject = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_COMMENT_CREATE, options); + createdComment = Comment.fromJSONObject(jsonObject); + } catch (ApiCallException | ClassCastException | IOException | JSONException ex) { + error = ex; + } + + return createdComment; + } + + protected void onPostExecute(Comment createdComment) { + Helper.setViewVisibility(progressView, View.GONE); + if (handler != null) { + if (createdComment != null) { + handler.onSuccess(createdComment); + } else { + handler.onError(error); + } + } + } + + public interface CommentCreateWithTipHandler { + void onSuccess(Comment createdComment); + void onError(Exception error); + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/CommentListHandler.java b/app/src/main/java/io/lbry/browser/tasks/CommentListHandler.java index a84a8585..d5afbe57 100644 --- a/app/src/main/java/io/lbry/browser/tasks/CommentListHandler.java +++ b/app/src/main/java/io/lbry/browser/tasks/CommentListHandler.java @@ -5,6 +5,6 @@ import java.util.List; import io.lbry.browser.model.Comment; public interface CommentListHandler { - void onSuccess(List comments); + void onSuccess(List comments, boolean hasReachedEnd); void onError(Exception error); } diff --git a/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java b/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java index e64f0308..61d49c4b 100644 --- a/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/CommentListTask.java @@ -52,9 +52,27 @@ public class CommentListTask extends AsyncTask> { JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_COMMENT_LIST, options); JSONArray items = result.getJSONArray("items"); + + List children = new ArrayList<>(); comments = new ArrayList<>(); for (int i = 0; i < items.length(); i++) { - comments.add(Comment.fromJSONObject(items.getJSONObject(i))); + Comment comment = Comment.fromJSONObject(items.getJSONObject(i)); + if (comment != null) { + if (!Helper.isNullOrEmpty(comment.getParentId())) { + children.add(comment); + } else { + comments.add(comment); + } + } + } + + for (Comment child : children) { + for (Comment parent : comments) { + if (parent.getId().equalsIgnoreCase(child.getParentId())) { + parent.addReply(child); + break; + } + } } } catch (Exception ex) { error = ex; @@ -65,12 +83,10 @@ public class CommentListTask extends AsyncTask> { protected void onPostExecute(List comments) { Helper.setViewVisibility(progressBar, View.GONE); if (handler != null) { - if (comments != null && error == null) { - handler.onSuccess(comments); + if (comments != null) { + handler.onSuccess(comments, comments.size() < pageSize); } else { handler.onError(error); - if (error != null) { - } } } } diff --git a/app/src/main/java/io/lbry/browser/tasks/wallet/SupportCreateTask.java b/app/src/main/java/io/lbry/browser/tasks/wallet/SupportCreateTask.java index d571898a..d11fb937 100644 --- a/app/src/main/java/io/lbry/browser/tasks/wallet/SupportCreateTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/wallet/SupportCreateTask.java @@ -40,6 +40,7 @@ public class SupportCreateTask extends AsyncTask { protected Boolean doInBackground(Void... params) { try { Map options = new HashMap<>(); + options.put("blocking", true); options.put("claim_id", claimId); options.put("amount", new DecimalFormat(Helper.SDK_AMOUNT_FORMAT, new DecimalFormatSymbols(Locale.US)).format(amount.doubleValue())); options.put("tip", tip); diff --git a/app/src/main/java/io/lbry/browser/ui/BaseFragment.java b/app/src/main/java/io/lbry/browser/ui/BaseFragment.java index 3d7cfe02..fe9f0669 100644 --- a/app/src/main/java/io/lbry/browser/ui/BaseFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/BaseFragment.java @@ -1,19 +1,36 @@ package io.lbry.browser.ui; import android.content.Context; +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Bundle; import android.view.View; import android.widget.TextView; +import androidx.appcompat.widget.AppCompatSpinner; import androidx.fragment.app.Fragment; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputEditText; + +import java.math.BigDecimal; import java.util.Map; +import io.lbry.browser.BuildConfig; import io.lbry.browser.MainActivity; import io.lbry.browser.R; +import io.lbry.browser.adapter.InlineChannelSpinnerAdapter; +import io.lbry.browser.model.Claim; import io.lbry.browser.model.WalletBalance; +import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask; +import io.lbry.browser.tasks.claim.ClaimResultHandler; +import io.lbry.browser.tasks.lbryinc.LogPublishTask; import io.lbry.browser.ui.wallet.RewardsFragment; import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Lbry; +import io.lbry.browser.utils.LbryAnalytics; +import io.lbry.browser.utils.LbryUri; import lombok.Getter; import lombok.Setter; @@ -94,4 +111,132 @@ public class BaseFragment extends Fragment { } } } + + public void showError(String message) { + Context context = getContext(); + if (context != null) { + Snackbar.make(getView(), message, Snackbar.LENGTH_LONG). + setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); + } + } + + public void setupInlineChannelCreator( + View container, + TextInputEditText inputChannelName, + TextInputEditText inputDeposit, + View inlineBalanceView, + TextView inlineBalanceValue, + View linkCancel, + MaterialButton buttonCreate, + View progressView, + AppCompatSpinner channelSpinner, + InlineChannelSpinnerAdapter channelSpinnerAdapter) { + inputDeposit.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean hasFocus) { + Helper.setViewVisibility(inlineBalanceView, hasFocus ? View.VISIBLE : View.INVISIBLE); + } + }); + + linkCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Helper.setViewText(inputChannelName, null); + Helper.setViewText(inputDeposit, null); + Helper.setViewVisibility(container, View.GONE); + } + }); + + buttonCreate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // validate deposit and channel name + String channelNameString = Helper.normalizeChannelName(Helper.getValue(inputChannelName.getText())); + Claim claimToSave = new Claim(); + claimToSave.setName(channelNameString); + String channelName = claimToSave.getName().startsWith("@") ? claimToSave.getName().substring(1) : claimToSave.getName(); + String depositString = Helper.getValue(inputDeposit.getText()); + if ("@".equals(channelName) || Helper.isNullOrEmpty(channelName)) { + showError(getString(R.string.please_enter_channel_name)); + return; + } + if (!LbryUri.isNameValid(channelName)) { + showError(getString(R.string.channel_name_invalid_characters)); + return; + } + if (Helper.channelExists(channelName)) { + showError(getString(R.string.channel_name_already_created)); + return; + } + + double depositAmount = 0; + try { + depositAmount = Double.valueOf(depositString); + } catch (NumberFormatException ex) { + // pass + showError(getString(R.string.please_enter_valid_deposit)); + return; + } + if (depositAmount == 0) { + String error = getResources().getQuantityString(R.plurals.min_deposit_required, depositAmount == 1 ? 1 : 2, String.valueOf(Helper.MIN_DEPOSIT)); + showError(error); + return; + } + if (Lbry.walletBalance == null || Lbry.walletBalance.getAvailable().doubleValue() < depositAmount) { + showError(getString(R.string.deposit_more_than_balance)); + return; + } + + ChannelCreateUpdateTask task = new ChannelCreateUpdateTask( + claimToSave, new BigDecimal(depositString), false, progressView, new ClaimResultHandler() { + @Override + public void beforeStart() { + Helper.setViewEnabled(inputChannelName, false); + Helper.setViewEnabled(inputDeposit, false); + Helper.setViewEnabled(buttonCreate, false); + Helper.setViewEnabled(linkCancel, false); + } + + @Override + public void onSuccess(Claim claimResult) { + if (!BuildConfig.DEBUG) { + LogPublishTask logPublishTask = new LogPublishTask(claimResult); + logPublishTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + // channel created + Bundle bundle = new Bundle(); + bundle.putString("claim_id", claimResult.getClaimId()); + bundle.putString("claim_name", claimResult.getName()); + LbryAnalytics.logEvent(LbryAnalytics.EVENT_CHANNEL_CREATE, bundle); + + // add the claim to the channel list and set it as the selected item + if (channelSpinnerAdapter != null) { + channelSpinnerAdapter.add(claimResult); + } + if (channelSpinner != null && channelSpinnerAdapter != null) { + channelSpinner.setSelection(channelSpinnerAdapter.getCount() - 1); + } + + Helper.setViewEnabled(inputChannelName, true); + Helper.setViewEnabled(inputDeposit, true); + Helper.setViewEnabled(buttonCreate, true); + Helper.setViewEnabled(linkCancel, true); + } + + @Override + public void onError(Exception error) { + Helper.setViewEnabled(inputChannelName, true); + Helper.setViewEnabled(inputDeposit, true); + Helper.setViewEnabled(buttonCreate, true); + Helper.setViewEnabled(linkCancel, true); + showError(error.getMessage()); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + }); + + Helper.setViewText(inlineBalanceValue, Helper.shortCurrencyFormat(Lbry.walletBalance.getAvailable().doubleValue())); + } } diff --git a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java index c7a5fc49..88cba35d 100644 --- a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java @@ -371,13 +371,6 @@ public class ChannelFormFragment extends BaseFragment implements task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - private void showError(String message) { - Context context = getContext(); - if (context != null) { - Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); - } - } - public void checkPermissionsAndLaunchFilePicker(boolean isCover) { Context context = getContext(); if (MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context)) { diff --git a/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java b/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java index f4c1674d..05b29ea7 100644 --- a/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java @@ -13,6 +13,8 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; import android.text.format.DateUtils; import android.view.ContextMenu; import android.view.LayoutInflater; @@ -24,6 +26,7 @@ import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; @@ -31,6 +34,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatSpinner; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; @@ -40,6 +44,7 @@ import androidx.webkit.WebSettingsCompat; import androidx.webkit.WebViewFeature; import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; import com.github.chrisbanes.photoview.PhotoView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultControlDispatcher; @@ -64,6 +69,7 @@ import com.google.android.exoplayer2.util.Util; import com.google.android.flexbox.FlexboxLayoutManager; import com.google.android.material.button.MaterialButton; import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputEditText; import org.commonmark.node.Node; import org.commonmark.parser.Parser; @@ -90,6 +96,7 @@ import io.lbry.browser.MainActivity; import io.lbry.browser.R; import io.lbry.browser.adapter.ClaimListAdapter; import io.lbry.browser.adapter.CommentListAdapter; +import io.lbry.browser.adapter.InlineChannelSpinnerAdapter; import io.lbry.browser.adapter.TagListAdapter; import io.lbry.browser.dialog.RepostClaimDialogFragment; import io.lbry.browser.dialog.SendTipDialogFragment; @@ -112,6 +119,7 @@ import io.lbry.browser.model.UrlSuggestion; import io.lbry.browser.model.WalletBalance; import io.lbry.browser.model.lbryinc.Reward; import io.lbry.browser.model.lbryinc.Subscription; +import io.lbry.browser.tasks.CommentCreateWithTipTask; import io.lbry.browser.tasks.CommentListHandler; import io.lbry.browser.tasks.CommentListTask; import io.lbry.browser.tasks.GenericTaskHandler; @@ -121,6 +129,7 @@ import io.lbry.browser.tasks.SetSdkSettingTask; import io.lbry.browser.tasks.claim.AbandonHandler; import io.lbry.browser.tasks.claim.AbandonStreamTask; import io.lbry.browser.tasks.claim.ClaimListResultHandler; +import io.lbry.browser.tasks.claim.ClaimListTask; import io.lbry.browser.tasks.claim.ClaimSearchResultHandler; import io.lbry.browser.tasks.claim.ResolveTask; import io.lbry.browser.tasks.file.DeleteFileTask; @@ -191,6 +200,28 @@ public class FileViewFragment extends BaseFragment implements private WebView webView; private boolean webViewAdded; + private boolean postingComment; + private boolean fetchingChannels; + private View progressLoadingChannels; + private View progressPostComment; + private InlineChannelSpinnerAdapter commentChannelSpinnerAdapter; + private AppCompatSpinner commentChannelSpinner; + private TextInputEditText inputComment; + private TextView textCommentLimit; + private MaterialButton buttonPostComment; + private ImageView commentPostAsThumbnail; + private View commentPostAsNoThumbnail; + private TextView commentPostAsAlpha; + + private View inlineChannelCreator; + private TextInputEditText inlineChannelCreatorInputName; + private TextInputEditText inlineChannelCreatorInputDeposit; + private View inlineChannelCreatorInlineBalance; + private TextView inlineChannelCreatorInlineBalanceValue; + private View inlineChannelCreatorCancelLink; + private View inlineChannelCreatorProgress; + private MaterialButton inlineChannelCreatorCreateButton; + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_file_view, container, false); @@ -201,6 +232,25 @@ public class FileViewFragment extends BaseFragment implements layoutDisplayArea = root.findViewById(R.id.file_view_claim_display_area); buttonPublishSomething = root.findViewById(R.id.nothing_at_location_publish_button); + commentChannelSpinner = root.findViewById(R.id.comment_form_channel_spinner); + progressLoadingChannels = root.findViewById(R.id.comment_form_channels_loading); + progressPostComment = root.findViewById(R.id.comment_form_post_progress); + inputComment = root.findViewById(R.id.comment_form_body); + textCommentLimit = root.findViewById(R.id.comment_form_text_limit); + buttonPostComment = root.findViewById(R.id.comment_form_post); + commentPostAsThumbnail = root.findViewById(R.id.comment_form_thumbnail); + commentPostAsNoThumbnail = root.findViewById(R.id.comment_form_no_thumbnail); + commentPostAsAlpha = root.findViewById(R.id.comment_form_thumbnail_alpha); + + inlineChannelCreator = root.findViewById(R.id.container_inline_channel_form_create); + inlineChannelCreatorInputName = root.findViewById(R.id.inline_channel_form_input_name); + inlineChannelCreatorInputDeposit = root.findViewById(R.id.inline_channel_form_input_deposit); + inlineChannelCreatorInlineBalance = root.findViewById(R.id.inline_channel_form_inline_balance_container); + inlineChannelCreatorInlineBalanceValue = root.findViewById(R.id.inline_channel_form_inline_balance_value); + inlineChannelCreatorProgress = root.findViewById(R.id.inline_channel_form_create_progress); + inlineChannelCreatorCancelLink = root.findViewById(R.id.inline_channel_form_cancel_link); + inlineChannelCreatorCreateButton = root.findViewById(R.id.inline_channel_form_create_button); + initUi(root); fileViewPlayerListener = new Player.EventListener() { @@ -457,6 +507,7 @@ public class FileViewFragment extends BaseFragment implements loadFile(); } checkOwnClaim(); + fetchChannels(); checkAndLoadComments(); } @@ -500,6 +551,10 @@ public class FileViewFragment extends BaseFragment implements if (!claim.isPlayable() && !claim.isViewable()) { showUnsupportedView(); } + } else { + if (!claim.isPlayable() && !claim.isViewable()) { + restoreMainActionButton(); + } } initialFileLoadDone = true; @@ -997,6 +1052,23 @@ public class FileViewFragment extends BaseFragment implements } }); + commentChannelSpinnerAdapter = new InlineChannelSpinnerAdapter(getContext(), R.layout.spinner_item_channel, new ArrayList<>()); + commentChannelSpinnerAdapter.addPlaceholder(false); + + initCommentForm(root); + setupInlineChannelCreator( + inlineChannelCreator, + inlineChannelCreatorInputName, + inlineChannelCreatorInputDeposit, + inlineChannelCreatorInlineBalance, + inlineChannelCreatorInlineBalanceValue, + inlineChannelCreatorCancelLink, + inlineChannelCreatorCreateButton, + inlineChannelCreatorProgress, + commentChannelSpinner, + commentChannelSpinnerAdapter + ); + RecyclerView relatedContentList = root.findViewById(R.id.file_view_related_content_list); RecyclerView commentsList = root.findViewById(R.id.file_view_comments_list); relatedContentList.setNestedScrollingEnabled(false); @@ -1237,15 +1309,7 @@ public class FileViewFragment extends BaseFragment implements Claim.GenericMetadata metadata = claim.getValue(); if (!Helper.isNullOrEmpty(claim.getThumbnailUrl())) { ImageView thumbnailView = root.findViewById(R.id.file_view_thumbnail); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - if (claim != null && context != null && thumbnailView != null) { - Glide.with(context.getApplicationContext()).asBitmap().load(claim.getThumbnailUrl()).centerCrop().into(thumbnailView); - } - } - }, 200); - + Glide.with(context.getApplicationContext()).asBitmap().load(claim.getThumbnailUrl()).centerCrop().into(thumbnailView); } else { // display first x letters of claim name, with random background } @@ -1891,33 +1955,75 @@ public class FileViewFragment extends BaseFragment implements if (claim != null && root != null) { CommentListTask relatedTask = new CommentListTask(1, 999, claim.getClaimId(), relatedLoading, new CommentListHandler() { @Override - public void onSuccess(List comments) { + public void onSuccess(List comments, boolean hasReachedEnd) { Context ctx = getContext(); - if (ctx != null) { + View root = getView(); + if (ctx != null && root != null) { commentListAdapter = new CommentListAdapter(comments, ctx); + commentListAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() { + @Override + public void onClaimClicked(Claim claim) { + if (!Helper.isNullOrEmpty(claim.getName()) && + claim.getName().startsWith("@") && + ctx instanceof MainActivity) { + ((MainActivity) ctx).openChannelClaim(claim); + } + } + }); - View v = getView(); - if (v != null) { - RecyclerView relatedContentList = root.findViewById(R.id.file_view_comments_list); - relatedContentList.setAdapter(commentListAdapter); - commentListAdapter.notifyDataSetChanged(); + RecyclerView relatedContentList = root.findViewById(R.id.file_view_comments_list); + relatedContentList.setAdapter(commentListAdapter); + commentListAdapter.notifyDataSetChanged(); - Helper.setViewVisibility( - v.findViewById(R.id.file_view_no_comments), - commentListAdapter == null || commentListAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - } + checkNoComments(); + resolveCommentPosters(); } } @Override public void onError(Exception error) { - + // pass } }); relatedTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } + private void checkNoComments() { + View root = getView(); + if (root != null) { + Helper.setViewVisibility(root.findViewById(R.id.file_view_no_comments), + commentListAdapter == null || commentListAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + } + + private void resolveCommentPosters() { + if (commentListAdapter != null) { + List urlsToResolve = new ArrayList<>(commentListAdapter.getClaimUrlsToResolve()); + if (urlsToResolve.size() > 0) { + ResolveTask task = new ResolveTask(urlsToResolve, Lbry.SDK_CONNECTION_STRING, null, new ClaimListResultHandler() { + @Override + public void onSuccess(List claims) { + if (commentListAdapter != null) { + for (Claim claim : claims) { + if (claim.getClaimId() != null) { + commentListAdapter.updatePosterForComment(claim.getClaimId(), claim); + } + } + commentListAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onError(Exception error) { + // pass + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + } + public boolean onBackPressed() { if (isInFullscreenMode()) { disableFullScreenMode(); @@ -2333,6 +2439,9 @@ public class FileViewFragment extends BaseFragment implements @Override public void onWalletBalanceUpdated(WalletBalance walletBalance) { + if (walletBalance != null && inlineChannelCreatorInlineBalanceValue != null) { + inlineChannelCreatorInlineBalanceValue.setText(Helper.shortCurrencyFormat(walletBalance.getAvailable().doubleValue())); + } checkRewardsDriver(); } @@ -2454,4 +2563,257 @@ public class FileViewFragment extends BaseFragment implements setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); } } + + private void fetchChannels() { + if (Lbry.ownChannels != null && Lbry.ownChannels.size() > 0) { + updateChannelList(Lbry.ownChannels); + return; + } + + fetchingChannels = true; + disableChannelSpinner(); + ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, progressLoadingChannels, new ClaimListResultHandler() { + @Override + public void onSuccess(List claims) { + Lbry.ownChannels = new ArrayList<>(claims); + updateChannelList(Lbry.ownChannels); + enableChannelSpinner(); + fetchingChannels = false; + } + + @Override + public void onError(Exception error) { + enableChannelSpinner(); + fetchingChannels = false; + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + private void disableChannelSpinner() { + Helper.setViewEnabled(commentChannelSpinner, false); + hideInlineChannelCreator(); + } + private void enableChannelSpinner() { + Helper.setViewEnabled(commentChannelSpinner, true); + if (commentChannelSpinner != null) { + Claim selectedClaim = (Claim) commentChannelSpinner.getSelectedItem(); + if (selectedClaim != null) { + if (selectedClaim.isPlaceholder()) { + showInlineChannelCreator(); + } else { + hideInlineChannelCreator(); + } + } + } + } + private void showInlineChannelCreator() { + Helper.setViewVisibility(inlineChannelCreator, View.VISIBLE); + } + private void hideInlineChannelCreator() { + Helper.setViewVisibility(inlineChannelCreator, View.GONE); + } + + private void updateChannelList(List channels) { + if (commentChannelSpinnerAdapter == null) { + Context context = getContext(); + if (context != null) { + commentChannelSpinnerAdapter = new InlineChannelSpinnerAdapter(context, R.layout.spinner_item_channel, new ArrayList<>(channels)); + commentChannelSpinnerAdapter.addPlaceholder(false); + commentChannelSpinnerAdapter.notifyDataSetChanged(); + } + } else { + commentChannelSpinnerAdapter.clear(); + commentChannelSpinnerAdapter.addAll(channels); + commentChannelSpinnerAdapter.addPlaceholder(false); + commentChannelSpinnerAdapter.notifyDataSetChanged(); + } + + if (commentChannelSpinner != null) { + commentChannelSpinner.setAdapter(commentChannelSpinnerAdapter); + } + + if (commentChannelSpinnerAdapter != null && commentChannelSpinner != null) { + if (commentChannelSpinnerAdapter.getCount() > 1) { + commentChannelSpinner.setSelection(1); + } + } + } + + private void initCommentForm(View root) { + double amount = Comment.COST / Lbryio.LBCUSDRate; + String buttonText = getResources().getQuantityString(R.plurals.post_for_credits, amount == 1 ? 1 : 2, Helper.LBC_CURRENCY_FORMAT.format(amount)); + buttonPostComment.setText(buttonText); + textCommentLimit.setText(String.format("%d / %d", Helper.getValue(inputComment.getText()).length(), Comment.MAX_LENGTH)); + + buttonPostComment.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (!Lbry.SDK_READY) { + Snackbar.make(root.findViewById(R.id.file_view_claim_display_area), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show(); + return; + } + + validateAndCheckPostComment(amount); + } + }); + + inputComment.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) { + int len = charSequence.length(); + textCommentLimit.setText(String.format("%d / %d", len, Comment.MAX_LENGTH)); + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + + commentChannelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, long l) { + Object item = adapterView.getItemAtPosition(position); + if (item instanceof Claim) { + Claim claim = (Claim) item; + if (claim.isPlaceholder()) { + if (!fetchingChannels) { + showInlineChannelCreator(); + } + } else { + hideInlineChannelCreator(); + updatePostAsChannel(claim); + } + } + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + }); + } + + private void validateAndCheckPostComment(double amount) { + String comment = Helper.getValue(inputComment.getText()); + Claim channel = (Claim) commentChannelSpinner.getSelectedItem(); + + if (Helper.isNullOrEmpty(comment)) { + showError(getString(R.string.please_enter_comment)); + return; + } + if (channel == null || Helper.isNullOrEmpty(channel.getClaimId())) { + showError(getString(R.string.please_select_channel)); + return; + } + if (Lbry.walletBalance == null || amount > Lbry.walletBalance.getAvailable().doubleValue()) { + showError(getString(R.string.insufficient_balance)); + return; + } + + Context context = getContext(); + if (context != null) { + String confirmText = getResources().getQuantityString( + R.plurals.confirm_post_comment, + amount == 1 ? 1 : 2, + Helper.LBC_CURRENCY_FORMAT.format(amount), + claim.getTitleOrName()); + AlertDialog.Builder builder = new AlertDialog.Builder(context). + setTitle(R.string.post_comment). + setMessage(confirmText) + .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + postComment(amount); + } + }).setNegativeButton(R.string.no, null); + builder.show(); + } + } + + private void updatePostAsChannel(Claim channel) { + boolean hasThumbnail = !Helper.isNullOrEmpty(channel.getThumbnailUrl()); + Helper.setViewVisibility(commentPostAsThumbnail, hasThumbnail ? View.VISIBLE : View.INVISIBLE); + Helper.setViewVisibility(commentPostAsNoThumbnail, !hasThumbnail ? View.VISIBLE : View.INVISIBLE); + Helper.setViewText(commentPostAsAlpha, channel.getName() != null ? channel.getName().substring(1, 2).toUpperCase() : null); + + Context context = getContext(); + int bgColor = Helper.generateRandomColorForValue(channel.getClaimId()); + Helper.setIconViewBackgroundColor(commentPostAsNoThumbnail, bgColor, false, context); + + if (hasThumbnail && context != null) { + Glide.with(context.getApplicationContext()). + asBitmap(). + load(channel.getThumbnailUrl()). + apply(RequestOptions.circleCropTransform()). + into(commentPostAsThumbnail); + } + } + + private void beforePostComment() { + postingComment = true; + Helper.setViewEnabled(commentChannelSpinner, false); + Helper.setViewEnabled(inputComment, false); + Helper.setViewEnabled(buttonPostComment, false); + } + + private void afterPostComment() { + Helper.setViewEnabled(commentChannelSpinner, true); + Helper.setViewEnabled(inputComment, true); + Helper.setViewEnabled(buttonPostComment, true); + postingComment = false; + } + + private Comment buildPostComment() { + Comment comment = new Comment(); + Claim channel = (Claim) commentChannelSpinner.getSelectedItem(); + comment.setClaimId(claim.getClaimId()); + comment.setChannelId(channel.getClaimId()); + comment.setChannelName(channel.getName()); + comment.setText(Helper.getValue(inputComment.getText())); + comment.setPoster(channel); + + return comment; + } + + private void postComment(double tipAmount) { + if (postingComment) { + return; + } + + Comment comment = buildPostComment(); + // only use 2 decimal places + BigDecimal amount = new BigDecimal(new DecimalFormat(Helper.PLAIN_CURRENCY_FORMAT_PATTERN).format(tipAmount)); + + beforePostComment(); + CommentCreateWithTipTask task = new CommentCreateWithTipTask(comment, amount, progressPostComment, new CommentCreateWithTipTask.CommentCreateWithTipHandler() { + @Override + public void onSuccess(Comment createdComment) { + inputComment.setText(null); + if (commentListAdapter != null) { + createdComment.setPoster(comment.getPoster()); + commentListAdapter.insert(0, createdComment); + } + afterPostComment(); + checkNoComments(); + + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).showMessage(R.string.comment_posted); + } + } + + @Override + public void onError(Exception error) { + showError(error.getMessage()); + afterPostComment(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } } diff --git a/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java b/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java index 0c5a259e..7b5c6324 100644 --- a/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java @@ -475,6 +475,8 @@ public class PublishFormFragment extends BaseFragment implements } }); + channelSpinnerAdapter = new InlineChannelSpinnerAdapter(getContext(), R.layout.spinner_item_channel, new ArrayList<>()); + channelSpinnerAdapter.addPlaceholder(false); setupInlineChannelCreator( inlineChannelCreator, inlineChannelCreatorInputName, @@ -483,7 +485,9 @@ public class PublishFormFragment extends BaseFragment implements inlineChannelCreatorInlineBalanceValue, inlineChannelCreatorCancelLink, inlineChannelCreatorCreateButton, - inlineChannelCreatorProgress + inlineChannelCreatorProgress, + channelSpinner, + channelSpinnerAdapter ); } @@ -1269,128 +1273,6 @@ public class PublishFormFragment extends BaseFragment implements checkRewardsDriver(); } - private void setupInlineChannelCreator( - View container, - TextInputEditText inputChannelName, - TextInputEditText inputDeposit, - View inlineBalanceView, - TextView inlineBalanceValue, - View linkCancel, - MaterialButton buttonCreate, - View progressView) { - inputDeposit.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - Helper.setViewVisibility(inlineBalanceView, hasFocus ? View.VISIBLE : View.INVISIBLE); - } - }); - - linkCancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Helper.setViewText(inputChannelName, null); - Helper.setViewText(inputDeposit, null); - Helper.setViewVisibility(container, View.GONE); - } - }); - - buttonCreate.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // validate deposit and channel name - String channelNameString = Helper.normalizeChannelName(Helper.getValue(inputChannelName.getText())); - Claim claimToSave = new Claim(); - claimToSave.setName(channelNameString); - String channelName = claimToSave.getName().startsWith("@") ? claimToSave.getName().substring(1) : claimToSave.getName(); - String depositString = Helper.getValue(inputDeposit.getText()); - if ("@".equals(channelName) || Helper.isNullOrEmpty(channelName)) { - showError(getString(R.string.please_enter_channel_name)); - return; - } - if (!LbryUri.isNameValid(channelName)) { - showError(getString(R.string.channel_name_invalid_characters)); - return; - } - if (Helper.channelExists(channelName)) { - showError(getString(R.string.channel_name_already_created)); - return; - } - - double depositAmount = 0; - try { - depositAmount = Double.valueOf(depositString); - } catch (NumberFormatException ex) { - // pass - showError(getString(R.string.please_enter_valid_deposit)); - return; - } - if (depositAmount == 0) { - String error = getResources().getQuantityString(R.plurals.min_deposit_required, depositAmount == 1 ? 1 : 2, String.valueOf(Helper.MIN_DEPOSIT)); - showError(error); - return; - } - if (Lbry.walletBalance == null || Lbry.walletBalance.getAvailable().doubleValue() < depositAmount) { - showError(getString(R.string.deposit_more_than_balance)); - return; - } - - ChannelCreateUpdateTask task = new ChannelCreateUpdateTask( - claimToSave, new BigDecimal(depositString), false, progressView, new ClaimResultHandler() { - @Override - public void beforeStart() { - Helper.setViewEnabled(inputChannelName, false); - Helper.setViewEnabled(inputDeposit, false); - Helper.setViewEnabled(buttonCreate, false); - Helper.setViewEnabled(linkCancel, false); - } - - @Override - public void onSuccess(Claim claimResult) { - if (!BuildConfig.DEBUG) { - LogPublishTask logPublishTask = new LogPublishTask(claimResult); - logPublishTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - // channel created - Bundle bundle = new Bundle(); - bundle.putString("claim_id", claimResult.getClaimId()); - bundle.putString("claim_name", claimResult.getName()); - LbryAnalytics.logEvent(LbryAnalytics.EVENT_CHANNEL_CREATE, bundle); - - // add the claim to the channel list and set it as the selected item - channelSpinnerAdapter.add(claimResult); - channelSpinner.setSelection(channelSpinnerAdapter.getCount() - 1); - - Helper.setViewEnabled(inputChannelName, true); - Helper.setViewEnabled(inputDeposit, true); - Helper.setViewEnabled(buttonCreate, true); - Helper.setViewEnabled(linkCancel, true); - } - - @Override - public void onError(Exception error) { - Helper.setViewEnabled(inputChannelName, true); - Helper.setViewEnabled(inputDeposit, true); - Helper.setViewEnabled(buttonCreate, true); - Helper.setViewEnabled(linkCancel, true); - showError(error.getMessage()); - } - }); - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - }); - - Helper.setViewText(inlineBalanceValue, Helper.shortCurrencyFormat(Lbry.walletBalance.getAvailable().doubleValue())); - } - - private void showError(String message) { - Context context = getContext(); - if (context != null) { - Snackbar.make(getView(), message, Snackbar.LENGTH_LONG). - setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); - } - } - private void checkUploadButton() { } diff --git a/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java b/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java index 76e37557..27d00f54 100644 --- a/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java @@ -248,6 +248,8 @@ public class InvitesFragment extends BaseFragment implements SdkStatusListener, } }); + channelSpinnerAdapter = new InlineChannelSpinnerAdapter(getContext(), R.layout.spinner_item_channel, new ArrayList<>()); + channelSpinnerAdapter.addPlaceholder(false); setupInlineChannelCreator( inlineChannelCreator, inlineChannelCreatorInputName, @@ -256,7 +258,9 @@ public class InvitesFragment extends BaseFragment implements SdkStatusListener, inlineChannelCreatorInlineBalanceValue, inlineChannelCreatorCancelLink, inlineChannelCreatorCreateButton, - inlineChannelCreatorProgress + inlineChannelCreatorProgress, + channelSpinner, + channelSpinnerAdapter ); } @@ -455,120 +459,6 @@ public class InvitesFragment extends BaseFragment implements SdkStatusListener, task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - private void setupInlineChannelCreator( - View container, - TextInputEditText inputChannelName, - TextInputEditText inputDeposit, - View inlineBalanceView, - TextView inlineBalanceValue, - View linkCancel, - MaterialButton buttonCreate, - View progressView) { - inputDeposit.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - Helper.setViewVisibility(inlineBalanceView, hasFocus ? View.VISIBLE : View.INVISIBLE); - } - }); - - linkCancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Helper.setViewText(inputChannelName, null); - Helper.setViewText(inputDeposit, null); - Helper.setViewVisibility(container, View.GONE); - } - }); - - buttonCreate.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // validate deposit and channel name - String channelNameString = Helper.normalizeChannelName(Helper.getValue(inputChannelName.getText())); - Claim claimToSave = new Claim(); - claimToSave.setName(channelNameString); - String channelName = claimToSave.getName().startsWith("@") ? claimToSave.getName().substring(1) : claimToSave.getName(); - String depositString = Helper.getValue(inputDeposit.getText()); - if ("@".equals(channelName) || Helper.isNullOrEmpty(channelName)) { - showError(getString(R.string.please_enter_channel_name)); - return; - } - if (!LbryUri.isNameValid(channelName)) { - showError(getString(R.string.channel_name_invalid_characters)); - return; - } - if (Helper.channelExists(channelName)) { - showError(getString(R.string.channel_name_already_created)); - return; - } - - double depositAmount = 0; - try { - depositAmount = Double.valueOf(depositString); - } catch (NumberFormatException ex) { - // pass - showError(getString(R.string.please_enter_valid_deposit)); - return; - } - if (depositAmount == 0) { - String error = getResources().getQuantityString(R.plurals.min_deposit_required, depositAmount == 1 ? 1 : 2, String.valueOf(Helper.MIN_DEPOSIT)); - showError(error); - return; - } - if (Lbry.walletBalance == null || Lbry.walletBalance.getAvailable().doubleValue() < depositAmount) { - showError(getString(R.string.deposit_more_than_balance)); - return; - } - - ChannelCreateUpdateTask task = new ChannelCreateUpdateTask( - claimToSave, new BigDecimal(depositString), false, progressView, new ClaimResultHandler() { - @Override - public void beforeStart() { - Helper.setViewEnabled(inputChannelName, false); - Helper.setViewEnabled(inputDeposit, false); - Helper.setViewEnabled(buttonCreate, false); - Helper.setViewEnabled(linkCancel, false); - } - - @Override - public void onSuccess(Claim claimResult) { - if (!BuildConfig.DEBUG) { - LogPublishTask logPublishTask = new LogPublishTask(claimResult); - logPublishTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - // channel created - Bundle bundle = new Bundle(); - bundle.putString("claim_id", claimResult.getClaimId()); - bundle.putString("claim_name", claimResult.getName()); - LbryAnalytics.logEvent(LbryAnalytics.EVENT_CHANNEL_CREATE, bundle); - - // add the claim to the channel list and set it as the selected item - channelSpinnerAdapter.add(claimResult); - channelSpinner.setSelection(channelSpinnerAdapter.getCount() - 1); - - Helper.setViewEnabled(inputChannelName, true); - Helper.setViewEnabled(inputDeposit, true); - Helper.setViewEnabled(buttonCreate, true); - Helper.setViewEnabled(linkCancel, true); - } - - @Override - public void onError(Exception error) { - Helper.setViewEnabled(inputChannelName, true); - Helper.setViewEnabled(inputDeposit, true); - Helper.setViewEnabled(buttonCreate, true); - Helper.setViewEnabled(linkCancel, true); - showError(error.getMessage()); - } - }); - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - }); - - Helper.setViewText(inlineBalanceValue, Helper.shortCurrencyFormat(Lbry.walletBalance.getAvailable().doubleValue())); - } - @Override public void onWalletBalanceUpdated(WalletBalance walletBalance) { if (walletBalance != null && inlineChannelCreatorInlineBalanceValue != null) { @@ -577,14 +467,6 @@ public class InvitesFragment extends BaseFragment implements SdkStatusListener, checkRewardsDriver(); } - private void showError(String message) { - Context context = getContext(); - if (context != null) { - Snackbar.make(getView(), message, Snackbar.LENGTH_LONG). - setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); - } - } - private void checkRewardsDriver() { Context ctx = getContext(); View root = getView(); diff --git a/app/src/main/java/io/lbry/browser/utils/Helper.java b/app/src/main/java/io/lbry/browser/utils/Helper.java index 0535eb5a..94632710 100644 --- a/app/src/main/java/io/lbry/browser/utils/Helper.java +++ b/app/src/main/java/io/lbry/browser/utils/Helper.java @@ -70,6 +70,7 @@ public final class Helper { public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8"); public static final int CONTENT_PAGE_SIZE = 25; public static final double MIN_DEPOSIT = 0.001; + public static final String PLAIN_CURRENCY_FORMAT_PATTERN = "####.##"; public static final String LBC_CURRENCY_FORMAT_PATTERN = "#,###.##"; public static final String FILE_SIZE_FORMAT_PATTERN = "#,###.#"; public static final DecimalFormat LBC_CURRENCY_FORMAT = new DecimalFormat(LBC_CURRENCY_FORMAT_PATTERN); diff --git a/app/src/main/java/io/lbry/browser/utils/Lbry.java b/app/src/main/java/io/lbry/browser/utils/Lbry.java index c9ff7460..27c3ce38 100644 --- a/app/src/main/java/io/lbry/browser/utils/Lbry.java +++ b/app/src/main/java/io/lbry/browser/utils/Lbry.java @@ -89,6 +89,8 @@ public final class Lbry { public static final String METHOD_PREFERENCE_GET = "preference_get"; public static final String METHOD_PREFERENCE_SET = "preference_set"; + public static final String METHOD_COMMENT_CREATE = "comment_create"; + public static final String METHOD_TXO_LIST = "txo_list"; public static final String METHOD_TXO_SPEND = "txo_spend"; diff --git a/app/src/main/res/layout/container_comment_form.xml b/app/src/main/res/layout/container_comment_form.xml new file mode 100644 index 00000000..41b16047 --- /dev/null +++ b/app/src/main/res/layout/container_comment_form.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_file_view.xml b/app/src/main/res/layout/fragment_file_view.xml index 9e335b04..02d38df2 100644 --- a/app/src/main/res/layout/fragment_file_view.xml +++ b/app/src/main/res/layout/fragment_file_view.xml @@ -37,7 +37,6 @@ @@ -45,8 +44,7 @@ android:id="@+id/file_view_media_container" android:background="@color/mediaContainerBackground" android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="3.55"> + android:layout_height="246dp"> @@ -623,7 +620,7 @@ android:id="@+id/file_view_comments_area" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginBottom="8dp" + android:paddingBottom="16dp" android:orientation="vertical"> - @@ -649,6 +645,8 @@ android:textSize="16sp" /> + + - + android:layout_height="wrap_content"> - + + + + + + - - + android:layout_toRightOf="@id/comment_avatar_container" + android:layout_marginLeft="16dp" + android:orientation="vertical"> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d742a0d..ef6c1105 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,8 +60,8 @@ Loading decentralized data... Related Content Comments - No comments to display. - Comments will display once the background service is done initializing. + No comments to display at this time. + Comments will display after the background service is initialized. Share LBRY content View Play @@ -81,6 +81,20 @@ Are you sure you want to unpublish this content? No files will be removed from your device. The content was successfully deleted from the blockchain. The content could not be deleted at this time. Please try again later. + Comment + Post as + Please enter a comment to post. + Please select a channel to post your comment as. + Post comment with tip? + Your comment was successfully posted. + + Post for %1$s credit + Post for %1$s credits + + + This will post your comment with a tip of %1$s credit for %2$s + This will post your comment with a tip of %1$s credits for %2$s + %1$s view %1$s views -- 2.45.2 From b1e8371d28834d3f73efdd944c575628e4c48b2a Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Sun, 31 May 2020 21:03:20 +0100 Subject: [PATCH 2/3] add comments to channel page --- .../browser/adapter/ClaimListAdapter.java | 8 +- .../browser/adapter/CommentListAdapter.java | 85 ++- .../ui/channel/ChannelCommentsFragment.java | 705 ++++++++++++++++++ .../browser/ui/channel/ChannelFragment.java | 15 +- .../ui/findcontent/FileViewFragment.java | 89 ++- .../res/layout/container_comment_form.xml | 273 ++++--- app/src/main/res/layout/fragment_channel.xml | 4 +- .../res/layout/fragment_channel_comments.xml | 71 ++ app/src/main/res/layout/list_item_comment.xml | 20 +- app/src/main/res/values/strings.xml | 2 + 10 files changed, 1148 insertions(+), 124 deletions(-) create mode 100644 app/src/main/java/io/lbry/browser/ui/channel/ChannelCommentsFragment.java create mode 100644 app/src/main/res/layout/fragment_channel_comments.xml diff --git a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java index 15651a8d..74f9df1b 100644 --- a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java @@ -297,17 +297,13 @@ public class ClaimListAdapter extends RecyclerView.Adapter { private List items; private Context context; + private boolean nested; + private float scale; @Setter private ClaimListAdapter.ClaimListItemListener listener; + @Setter + private ReplyClickListener replyListener; public CommentListAdapter(List items, Context context) { + this(items, context, false); + } + + public CommentListAdapter(List items, Context context, boolean nested) { this.items = new ArrayList<>(items); this.context = context; + this.nested = nested; + if (context != null) { + scale = context.getResources().getDisplayMetrics().density; + } for (Comment item : this.items) { ClaimCacheKey key = new ClaimCacheKey(); key.setClaimId(item.getChannelId()); @@ -43,6 +56,11 @@ public class CommentListAdapter extends RecyclerView.Adapter 0) { + for (int j = 0; j < item.getReplies().size(); j++) { + Comment reply = item.getReplies().get(j); + if (reply.getPoster() == null) { + LbryUri url = LbryUri.tryParse(String.format("%s#%s", reply.getChannelName(), reply.getChannelId())); + if (url != null && !urls.contains(url.toString())) { + urls.add(url.toString()); + } + } + } + } } return urls; } @@ -69,15 +98,19 @@ public class CommentListAdapter extends RecyclerView.Adapter replies = item.getReplies(); + if (replies != null && replies.size() > 0) { + for (int j = 0; j < replies.size(); j++) { + Comment reply = item.getReplies().get(j); + if (channelId.equalsIgnoreCase(item.getChannelId())) { + reply.setPoster(channel); + break; + } + } + } + if (channelId.equalsIgnoreCase(item.getChannelId())) { item.setPoster(channel); break; @@ -107,10 +162,17 @@ public class CommentListAdapter extends RecyclerView.Adapter replies = comment.getReplies(); + boolean hasReplies = replies != null && replies.size() > 0; + if (hasReplies) { + holder.repliesList.setLayoutManager(new LinearLayoutManager(context)); + holder.repliesList.setAdapter(new CommentListAdapter(replies, context, true)); + } else { + holder.repliesList.setAdapter(null); + } holder.channelName.setOnClickListener(new View.OnClickListener() { @Override @@ -131,5 +201,18 @@ public class CommentListAdapter extends RecyclerView.Adapter comments, boolean hasReachedEnd) { + Context ctx = getContext(); + View root = getView(); + if (ctx != null && root != null) { + commentListAdapter = new CommentListAdapter(comments, ctx); + commentListAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() { + @Override + public void onClaimClicked(Claim claim) { + if (!Helper.isNullOrEmpty(claim.getName()) && + claim.getName().startsWith("@") && + ctx instanceof MainActivity) { + ((MainActivity) ctx).openChannelClaim(claim); + } + } + }); + commentListAdapter.setReplyListener(new CommentListAdapter.ReplyClickListener() { + @Override + public void onReplyClicked(Comment comment) { + setReplyToComment(comment); + } + }); + + RecyclerView relatedContentList = root.findViewById(R.id.channel_comments_list); + relatedContentList.setAdapter(commentListAdapter); + commentListAdapter.notifyDataSetChanged(); + + checkNoComments(); + resolveCommentPosters(); + } + } + + @Override + public void onError(Exception error) { + // pass + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + + private void resolveCommentPosters() { + if (commentListAdapter != null) { + List urlsToResolve = new ArrayList<>(commentListAdapter.getClaimUrlsToResolve()); + if (urlsToResolve.size() > 0) { + ResolveTask task = new ResolveTask(urlsToResolve, Lbry.SDK_CONNECTION_STRING, null, new ClaimListResultHandler() { + @Override + public void onSuccess(List claims) { + if (commentListAdapter != null) { + for (Claim claim : claims) { + if (claim.getClaimId() != null) { + commentListAdapter.updatePosterForComment(claim.getClaimId(), claim); + } + } + commentListAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onError(Exception error) { + // pass + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + } + + @Override + public void onWalletBalanceUpdated(WalletBalance walletBalance) { + if (walletBalance != null && inlineChannelCreatorInlineBalanceValue != null) { + inlineChannelCreatorInlineBalanceValue.setText(Helper.shortCurrencyFormat(walletBalance.getAvailable().doubleValue())); + } + } + + private void fetchChannels() { + if (Lbry.ownChannels != null && Lbry.ownChannels.size() > 0) { + updateChannelList(Lbry.ownChannels); + return; + } + + fetchingChannels = true; + disableChannelSpinner(); + ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, progressLoadingChannels, new ClaimListResultHandler() { + @Override + public void onSuccess(List claims) { + Lbry.ownChannels = new ArrayList<>(claims); + updateChannelList(Lbry.ownChannels); + enableChannelSpinner(); + fetchingChannels = false; + } + + @Override + public void onError(Exception error) { + enableChannelSpinner(); + fetchingChannels = false; + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + private void disableChannelSpinner() { + Helper.setViewEnabled(commentChannelSpinner, false); + hideInlineChannelCreator(); + } + private void enableChannelSpinner() { + Helper.setViewEnabled(commentChannelSpinner, true); + if (commentChannelSpinner != null) { + Claim selectedClaim = (Claim) commentChannelSpinner.getSelectedItem(); + if (selectedClaim != null) { + if (selectedClaim.isPlaceholder()) { + showInlineChannelCreator(); + } else { + hideInlineChannelCreator(); + } + } + } + } + private void showInlineChannelCreator() { + Helper.setViewVisibility(inlineChannelCreator, View.VISIBLE); + } + private void hideInlineChannelCreator() { + Helper.setViewVisibility(inlineChannelCreator, View.GONE); + } + + private void updateChannelList(List channels) { + if (commentChannelSpinnerAdapter == null) { + Context context = getContext(); + if (context != null) { + commentChannelSpinnerAdapter = new InlineChannelSpinnerAdapter(context, R.layout.spinner_item_channel, new ArrayList<>(channels)); + commentChannelSpinnerAdapter.addPlaceholder(false); + commentChannelSpinnerAdapter.notifyDataSetChanged(); + } + } else { + commentChannelSpinnerAdapter.clear(); + commentChannelSpinnerAdapter.addAll(channels); + commentChannelSpinnerAdapter.addPlaceholder(false); + commentChannelSpinnerAdapter.notifyDataSetChanged(); + } + + if (commentChannelSpinner != null) { + commentChannelSpinner.setAdapter(commentChannelSpinnerAdapter); + } + + if (commentChannelSpinnerAdapter != null && commentChannelSpinner != null) { + if (commentChannelSpinnerAdapter.getCount() > 1) { + commentChannelSpinner.setSelection(1); + } + } + } + + private void initCommentForm(View root) { + double amount = Comment.COST / Lbryio.LBCUSDRate; + String buttonText = getResources().getQuantityString(R.plurals.post_for_credits, amount == 1 ? 1 : 2, Helper.LBC_CURRENCY_FORMAT.format(amount)); + buttonPostComment.setText(buttonText); + textCommentLimit.setText(String.format("%d / %d", Helper.getValue(inputComment.getText()).length(), Comment.MAX_LENGTH)); + + buttonClearReplyToComment.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + clearReplyToComment(); + } + }); + + buttonPostComment.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (!Lbry.SDK_READY) { + Snackbar.make(root.findViewById(R.id.channel_comments_area), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show(); + return; + } + + validateAndCheckPostComment(amount); + } + }); + + inputComment.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) { + int len = charSequence.length(); + textCommentLimit.setText(String.format("%d / %d", len, Comment.MAX_LENGTH)); + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + + commentChannelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, long l) { + Object item = adapterView.getItemAtPosition(position); + if (item instanceof Claim) { + Claim claim = (Claim) item; + if (claim.isPlaceholder()) { + if (!fetchingChannels) { + showInlineChannelCreator(); + } + } else { + hideInlineChannelCreator(); + updatePostAsChannel(claim); + } + } + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + }); + } + + private void validateAndCheckPostComment(double amount) { + String comment = Helper.getValue(inputComment.getText()); + Claim channel = (Claim) commentChannelSpinner.getSelectedItem(); + + if (Helper.isNullOrEmpty(comment)) { + showError(getString(R.string.please_enter_comment)); + return; + } + if (channel == null || Helper.isNullOrEmpty(channel.getClaimId())) { + showError(getString(R.string.please_select_channel)); + return; + } + if (Lbry.walletBalance == null || amount > Lbry.walletBalance.getAvailable().doubleValue()) { + showError(getString(R.string.insufficient_balance)); + return; + } + + Context context = getContext(); + if (context != null) { + String confirmText = getResources().getQuantityString( + R.plurals.confirm_post_comment, + amount == 1 ? 1 : 2, + Helper.LBC_CURRENCY_FORMAT.format(amount), + claim.getTitleOrName()); + AlertDialog.Builder builder = new AlertDialog.Builder(context). + setTitle(R.string.post_comment). + setMessage(confirmText) + .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + postComment(amount); + } + }).setNegativeButton(R.string.no, null); + builder.show(); + } + } + + private void updatePostAsChannel(Claim channel) { + boolean hasThumbnail = !Helper.isNullOrEmpty(channel.getThumbnailUrl()); + Helper.setViewVisibility(commentPostAsThumbnail, hasThumbnail ? View.VISIBLE : View.INVISIBLE); + Helper.setViewVisibility(commentPostAsNoThumbnail, !hasThumbnail ? View.VISIBLE : View.INVISIBLE); + Helper.setViewText(commentPostAsAlpha, channel.getName() != null ? channel.getName().substring(1, 2).toUpperCase() : null); + + Context context = getContext(); + int bgColor = Helper.generateRandomColorForValue(channel.getClaimId()); + Helper.setIconViewBackgroundColor(commentPostAsNoThumbnail, bgColor, false, context); + + if (hasThumbnail && context != null) { + Glide.with(context.getApplicationContext()). + asBitmap(). + load(channel.getThumbnailUrl()). + apply(RequestOptions.circleCropTransform()). + into(commentPostAsThumbnail); + } + } + + private void beforePostComment() { + postingComment = true; + Helper.setViewEnabled(commentChannelSpinner, false); + Helper.setViewEnabled(inputComment, false); + Helper.setViewEnabled(buttonClearReplyToComment, false); + Helper.setViewEnabled(buttonPostComment, false); + } + + private void afterPostComment() { + Helper.setViewEnabled(commentChannelSpinner, true); + Helper.setViewEnabled(inputComment, true); + Helper.setViewEnabled(buttonClearReplyToComment, true); + Helper.setViewEnabled(buttonPostComment, true); + postingComment = false; + } + + private Comment buildPostComment() { + Comment comment = new Comment(); + Claim channel = (Claim) commentChannelSpinner.getSelectedItem(); + comment.setClaimId(claim.getClaimId()); + comment.setChannelId(channel.getClaimId()); + comment.setChannelName(channel.getName()); + comment.setText(Helper.getValue(inputComment.getText())); + comment.setPoster(channel); + if (replyToComment != null) { + comment.setParentId(replyToComment.getId()); + } + + return comment; + } + + private void setReplyToComment(Comment comment) { + replyToComment = comment; + Helper.setViewText(textReplyingTo, getString(R.string.replying_to, comment.getChannelName())); + Helper.setViewText(textReplyToBody, comment.getText()); + Helper.setViewVisibility(containerReplyToComment, View.VISIBLE); + + inputComment.requestFocus(); + Context context = getContext(); + if (context != null) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(inputComment, InputMethodManager.SHOW_FORCED); + } + } + + private void clearReplyToComment() { + Helper.setViewText(textReplyingTo, null); + Helper.setViewText(textReplyToBody, null); + Helper.setViewVisibility(containerReplyToComment, View.GONE); + replyToComment = null; + } + + private void postComment(double tipAmount) { + if (postingComment) { + return; + } + + Comment comment = buildPostComment(); + // only use 2 decimal places + BigDecimal amount = new BigDecimal(new DecimalFormat(Helper.PLAIN_CURRENCY_FORMAT_PATTERN).format(tipAmount)); + + beforePostComment(); + CommentCreateWithTipTask task = new CommentCreateWithTipTask(comment, amount, progressPostComment, new CommentCreateWithTipTask.CommentCreateWithTipHandler() { + @Override + public void onSuccess(Comment createdComment) { + inputComment.setText(null); + clearReplyToComment(); + + if (commentListAdapter != null) { + createdComment.setPoster(comment.getPoster()); + if (!Helper.isNullOrEmpty(createdComment.getParentId())) { + commentListAdapter.addReply(createdComment); + } else { + commentListAdapter.insert(0, createdComment); + } + } + afterPostComment(); + checkNoComments(); + + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).showMessage(R.string.comment_posted); + } + } + + @Override + public void onError(Exception error) { + showError(error.getMessage()); + afterPostComment(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void showError(String message) { + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).showError(message); + } + } + + private void checkNoComments() { + View root = getView(); + if (root != null) { + Helper.setViewVisibility(root.findViewById(R.id.channel_no_comments), + commentListAdapter == null || commentListAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + } + + private void setupInlineChannelCreator( + View container, + TextInputEditText inputChannelName, + TextInputEditText inputDeposit, + View inlineBalanceView, + TextView inlineBalanceValue, + View linkCancel, + MaterialButton buttonCreate, + View progressView, + AppCompatSpinner channelSpinner, + InlineChannelSpinnerAdapter channelSpinnerAdapter) { + inputDeposit.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean hasFocus) { + Helper.setViewVisibility(inlineBalanceView, hasFocus ? View.VISIBLE : View.INVISIBLE); + } + }); + + linkCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Helper.setViewText(inputChannelName, null); + Helper.setViewText(inputDeposit, null); + Helper.setViewVisibility(container, View.GONE); + } + }); + + buttonCreate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // validate deposit and channel name + String channelNameString = Helper.normalizeChannelName(Helper.getValue(inputChannelName.getText())); + Claim claimToSave = new Claim(); + claimToSave.setName(channelNameString); + String channelName = claimToSave.getName().startsWith("@") ? claimToSave.getName().substring(1) : claimToSave.getName(); + String depositString = Helper.getValue(inputDeposit.getText()); + if ("@".equals(channelName) || Helper.isNullOrEmpty(channelName)) { + showError(getString(R.string.please_enter_channel_name)); + return; + } + if (!LbryUri.isNameValid(channelName)) { + showError(getString(R.string.channel_name_invalid_characters)); + return; + } + if (Helper.channelExists(channelName)) { + showError(getString(R.string.channel_name_already_created)); + return; + } + + double depositAmount = 0; + try { + depositAmount = Double.valueOf(depositString); + } catch (NumberFormatException ex) { + // pass + showError(getString(R.string.please_enter_valid_deposit)); + return; + } + if (depositAmount == 0) { + String error = getResources().getQuantityString(R.plurals.min_deposit_required, depositAmount == 1 ? 1 : 2, String.valueOf(Helper.MIN_DEPOSIT)); + showError(error); + return; + } + if (Lbry.walletBalance == null || Lbry.walletBalance.getAvailable().doubleValue() < depositAmount) { + showError(getString(R.string.deposit_more_than_balance)); + return; + } + + ChannelCreateUpdateTask task = new ChannelCreateUpdateTask( + claimToSave, new BigDecimal(depositString), false, progressView, new ClaimResultHandler() { + @Override + public void beforeStart() { + Helper.setViewEnabled(inputChannelName, false); + Helper.setViewEnabled(inputDeposit, false); + Helper.setViewEnabled(buttonCreate, false); + Helper.setViewEnabled(linkCancel, false); + } + + @Override + public void onSuccess(Claim claimResult) { + if (!BuildConfig.DEBUG) { + LogPublishTask logPublishTask = new LogPublishTask(claimResult); + logPublishTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + // channel created + Bundle bundle = new Bundle(); + bundle.putString("claim_id", claimResult.getClaimId()); + bundle.putString("claim_name", claimResult.getName()); + LbryAnalytics.logEvent(LbryAnalytics.EVENT_CHANNEL_CREATE, bundle); + + // add the claim to the channel list and set it as the selected item + if (channelSpinnerAdapter != null) { + channelSpinnerAdapter.add(claimResult); + } + if (channelSpinner != null && channelSpinnerAdapter != null) { + channelSpinner.setSelection(channelSpinnerAdapter.getCount() - 1); + } + + Helper.setViewEnabled(inputChannelName, true); + Helper.setViewEnabled(inputDeposit, true); + Helper.setViewEnabled(buttonCreate, true); + Helper.setViewEnabled(linkCancel, true); + } + + @Override + public void onError(Exception error) { + Helper.setViewEnabled(inputChannelName, true); + Helper.setViewEnabled(inputDeposit, true); + Helper.setViewEnabled(buttonCreate, true); + Helper.setViewEnabled(linkCancel, true); + showError(error.getMessage()); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + }); + + Helper.setViewText(inlineBalanceValue, Helper.shortCurrencyFormat(Lbry.walletBalance.getAvailable().doubleValue())); + } +} diff --git a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java index a389e273..76b9f2fd 100644 --- a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java @@ -446,7 +446,11 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen new TabLayoutMediator(tabLayout, tabPager, new TabLayoutMediator.TabConfigurationStrategy() { @Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) { - tab.setText(position == 0 ? R.string.content : R.string.about); + switch (position) { + case 0: tab.setText(R.string.content); break; + case 1: tab.setText(R.string.about); break; + case 2: tab.setText(R.string.comments); break; + } } }).attach(); } catch (IllegalStateException ex) { @@ -516,6 +520,13 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen // pass } return aboutFragment; + + case 2: + ChannelCommentsFragment commentsFragment = ChannelCommentsFragment.class.newInstance(); + if (channelClaim != null) { + commentsFragment.setClaim(channelClaim); + } + return commentsFragment; } return null; @@ -527,7 +538,7 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen @Override public int getItemCount() { - return 2; + return 3; } } } diff --git a/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java b/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java index 09948944..90ce3dfc 100644 --- a/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/findcontent/FileViewFragment.java @@ -12,7 +12,6 @@ import android.graphics.Color; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.text.format.DateUtils; @@ -22,6 +21,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; @@ -200,6 +200,12 @@ public class FileViewFragment extends BaseFragment implements private WebView webView; private boolean webViewAdded; + private Comment replyToComment; + private View containerReplyToComment; + private TextView textReplyingTo; + private TextView textReplyToBody; + private View buttonClearReplyToComment; + private boolean postingComment; private boolean fetchingChannels; private View progressLoadingChannels; @@ -232,6 +238,11 @@ public class FileViewFragment extends BaseFragment implements layoutDisplayArea = root.findViewById(R.id.file_view_claim_display_area); buttonPublishSomething = root.findViewById(R.id.nothing_at_location_publish_button); + containerReplyToComment = root.findViewById(R.id.comment_form_reply_to_container); + textReplyingTo = root.findViewById(R.id.comment_form_replying_to_text); + textReplyToBody = root.findViewById(R.id.comment_form_reply_to_body); + buttonClearReplyToComment = root.findViewById(R.id.comment_form_clear_reply_to); + commentChannelSpinner = root.findViewById(R.id.comment_form_channel_spinner); progressLoadingChannels = root.findViewById(R.id.comment_form_channels_loading); progressPostComment = root.findViewById(R.id.comment_form_post_progress); @@ -494,7 +505,14 @@ public class FileViewFragment extends BaseFragment implements View root = getView(); if (root != null) { + if (relatedContentAdapter != null) { + relatedContentAdapter.clearItems(); + } + if (commentListAdapter != null) { + commentListAdapter.clearItems(); + } ((RecyclerView) root.findViewById(R.id.file_view_related_content_list)).setAdapter(null); + ((RecyclerView) root.findViewById(R.id.file_view_comments_list)).setAdapter(null); } if (MainActivity.appPlayer != null) { MainActivity.appPlayer.setPlayWhenReady(false); @@ -641,6 +659,7 @@ public class FileViewFragment extends BaseFragment implements } else { onSdkReady(); } + checkCommentSdkInitializing(); } public void onStop() { @@ -1400,19 +1419,22 @@ public class FileViewFragment extends BaseFragment implements private void checkAndLoadComments() { View root = getView(); if (root != null) { + checkCommentSdkInitializing(); RecyclerView commentsList = root.findViewById(R.id.file_view_comments_list); if (commentsList == null || commentsList.getAdapter() == null || commentsList.getAdapter().getItemCount() == 0) { - TextView commentsSDKInitializing = root.findViewById(R.id.file_view_comments_sdk_initializing); - if (Lbry.SDK_READY) { - Helper.setViewVisibility(commentsSDKInitializing, View.GONE); - loadComments(); - } else { - Helper.setViewVisibility(commentsSDKInitializing, View.VISIBLE); - } + loadComments(); } } } + private void checkCommentSdkInitializing() { + View root = getView(); + if (root != null) { + TextView commentsSDKInitializing = root.findViewById(R.id.file_view_comments_sdk_initializing); + Helper.setViewVisibility(commentsSDKInitializing, Lbry.SDK_READY ? View.GONE : View.VISIBLE); + } + } + private void showUnsupportedView() { View root = getView(); if (root != null) { @@ -1956,7 +1978,7 @@ public class FileViewFragment extends BaseFragment implements View root = getView(); ProgressBar relatedLoading = root.findViewById(R.id.file_view_comments_progress); if (claim != null && root != null) { - CommentListTask relatedTask = new CommentListTask(1, 999, claim.getClaimId(), relatedLoading, new CommentListHandler() { + CommentListTask task = new CommentListTask(1, 500, claim.getClaimId(), relatedLoading, new CommentListHandler() { @Override public void onSuccess(List comments, boolean hasReachedEnd) { Context ctx = getContext(); @@ -1973,6 +1995,12 @@ public class FileViewFragment extends BaseFragment implements } } }); + commentListAdapter.setReplyListener(new CommentListAdapter.ReplyClickListener() { + @Override + public void onReplyClicked(Comment comment) { + setReplyToComment(comment); + } + }); RecyclerView relatedContentList = root.findViewById(R.id.file_view_comments_list); relatedContentList.setAdapter(commentListAdapter); @@ -1988,7 +2016,7 @@ public class FileViewFragment extends BaseFragment implements // pass } }); - relatedTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } @@ -2648,6 +2676,13 @@ public class FileViewFragment extends BaseFragment implements buttonPostComment.setText(buttonText); textCommentLimit.setText(String.format("%d / %d", Helper.getValue(inputComment.getText()).length(), Comment.MAX_LENGTH)); + buttonClearReplyToComment.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + clearReplyToComment(); + } + }); + buttonPostComment.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -2762,12 +2797,14 @@ public class FileViewFragment extends BaseFragment implements postingComment = true; Helper.setViewEnabled(commentChannelSpinner, false); Helper.setViewEnabled(inputComment, false); + Helper.setViewEnabled(buttonClearReplyToComment, false); Helper.setViewEnabled(buttonPostComment, false); } private void afterPostComment() { Helper.setViewEnabled(commentChannelSpinner, true); Helper.setViewEnabled(inputComment, true); + Helper.setViewEnabled(buttonClearReplyToComment, true); Helper.setViewEnabled(buttonPostComment, true); postingComment = false; } @@ -2780,10 +2817,34 @@ public class FileViewFragment extends BaseFragment implements comment.setChannelName(channel.getName()); comment.setText(Helper.getValue(inputComment.getText())); comment.setPoster(channel); + if (replyToComment != null) { + comment.setParentId(replyToComment.getId()); + } return comment; } + private void setReplyToComment(Comment comment) { + replyToComment = comment; + Helper.setViewText(textReplyingTo, getString(R.string.replying_to, comment.getChannelName())); + Helper.setViewText(textReplyToBody, comment.getText()); + Helper.setViewVisibility(containerReplyToComment, View.VISIBLE); + + inputComment.requestFocus(); + Context context = getContext(); + if (context != null) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(inputComment, InputMethodManager.SHOW_FORCED); + } + } + + private void clearReplyToComment() { + Helper.setViewText(textReplyingTo, null); + Helper.setViewText(textReplyToBody, null); + Helper.setViewVisibility(containerReplyToComment, View.GONE); + replyToComment = null; + } + private void postComment(double tipAmount) { if (postingComment) { return; @@ -2798,9 +2859,15 @@ public class FileViewFragment extends BaseFragment implements @Override public void onSuccess(Comment createdComment) { inputComment.setText(null); + clearReplyToComment(); + if (commentListAdapter != null) { createdComment.setPoster(comment.getPoster()); - commentListAdapter.insert(0, createdComment); + if (!Helper.isNullOrEmpty(createdComment.getParentId())) { + commentListAdapter.addReply(createdComment); + } else { + commentListAdapter.insert(0, createdComment); + } } afterPostComment(); checkNoComments(); diff --git a/app/src/main/res/layout/container_comment_form.xml b/app/src/main/res/layout/container_comment_form.xml index 41b16047..1948ef6d 100644 --- a/app/src/main/res/layout/container_comment_form.xml +++ b/app/src/main/res/layout/container_comment_form.xml @@ -1,118 +1,187 @@ - - - - - - - + - - - - + android:orientation="vertical" + android:padding="16dp"> - - - - + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + - - + + + + + + + - - + android:layout_marginLeft="8dp"> + + - + + - - + + + + + + + + + + + + + + + + + + + - - - \ No newline at end of file + android:layout_gravity="right" + android:layout_marginTop="2dp" + android:fontFamily="@font/inter" + android:textColor="@color/lightGrey" + android:textFontWeight="300" + android:textSize="12sp" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index bfac6090..af46977f 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -95,7 +95,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" - app:tabGravity="center" /> + android:paddingLeft="110dp" + app:tabGravity="fill" + app:tabMode="scrollable"/> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_comment.xml b/app/src/main/res/layout/list_item_comment.xml index 882ab739..bf068177 100644 --- a/app/src/main/res/layout/list_item_comment.xml +++ b/app/src/main/res/layout/list_item_comment.xml @@ -78,11 +78,29 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" - android:lineSpacingMultiplier="1.05" + android:lineSpacingMultiplier="1.1" android:fontFamily="@font/inter" android:textFontWeight="300" + android:textIsSelectable="true" android:textSize="14sp" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ea3f5a1..5805f84f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,8 @@ Post comment with tip? Your comment was successfully posted. Please select a channel to repost on. + Reply + Replying to %1$s Post for %1$s credit Post for %1$s credits -- 2.45.2 From 4685791f9c6e8e9c5708e461905fe14fd994409d Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Sun, 31 May 2020 21:05:07 +0100 Subject: [PATCH 3/3] fix adapter update poster method --- .../main/java/io/lbry/browser/adapter/CommentListAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java index f0cb3494..68754c98 100644 --- a/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/CommentListAdapter.java @@ -139,7 +139,7 @@ public class CommentListAdapter extends RecyclerView.Adapter 0) { for (int j = 0; j < replies.size(); j++) { Comment reply = item.getReplies().get(j); - if (channelId.equalsIgnoreCase(item.getChannelId())) { + if (channelId.equalsIgnoreCase(reply.getChannelId())) { reply.setPoster(channel); break; } -- 2.45.2