From 1d97f9008dd637ad2eba8502eb4a5a7afead9d6e Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Sun, 31 May 2020 21:06:03 +0100 Subject: [PATCH] implement commenting with tips (#922) * implement commenting with tips * add comments to channel page --- .../browser/adapter/ClaimListAdapter.java | 8 +- .../browser/adapter/CommentListAdapter.java | 168 ++++- .../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/ChannelCommentsFragment.java | 705 ++++++++++++++++++ .../ui/channel/ChannelFormFragment.java | 7 - .../browser/ui/channel/ChannelFragment.java | 15 +- .../ui/findcontent/FileViewFragment.java | 491 +++++++++++- .../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 | 187 +++++ app/src/main/res/layout/fragment_channel.xml | 4 +- .../res/layout/fragment_channel_comments.xml | 71 ++ .../main/res/layout/fragment_file_view.xml | 18 +- app/src/main/res/layout/list_item_comment.xml | 104 ++- app/src/main/res/values/strings.xml | 20 +- 22 files changed, 2041 insertions(+), 336 deletions(-) create mode 100644 app/src/main/java/io/lbry/browser/tasks/CommentCreateWithTipTask.java create mode 100644 app/src/main/java/io/lbry/browser/ui/channel/ChannelCommentsFragment.java create mode 100644 app/src/main/res/layout/container_comment_form.xml 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 = items; + 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()); + if (Lbry.claimCache.containsKey(key)) { + item.setPoster(Lbry.claimCache.get(key)); + } + } + } + + public void clearItems() { + items.clear(); + notifyDataSetChanged(); } @Override @@ -26,14 +66,90 @@ 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.contains(url.toString())) { + urls.add(url.toString()); + } + } + if (item.getReplies().size() > 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; + } + 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; + protected View replyLink; + protected RecyclerView repliesList; 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); + replyLink = v.findViewById(R.id.comment_reply_link); + thumbnailView = v.findViewById(R.id.comment_thumbnail); + noThumbnailView = v.findViewById(R.id.comment_no_thumbnail); + alphaView = v.findViewById(R.id.comment_thumbnail_alpha); + repliesList = v.findViewById(R.id.comment_replies); + } + } + + public void insert(int index, Comment comment) { + if (!items.contains(comment)) { + items.add(index, comment); + notifyDataSetChanged(); + } + } + + public void addReply(Comment comment) { + for (int i = 0; i < items.size(); i++) { + Comment parent = items.get(i); + if (parent.getId().equalsIgnoreCase(comment.getParentId())) { + parent.addReply(comment); + notifyDataSetChanged(); + break; + } + } + } + + public void updatePosterForComment(String channelId, Claim channel) { + for (int i = 0 ; i < items.size(); i++) { + Comment item = items.get(i); + List 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(reply.getChannelId())) { + reply.setPoster(channel); + break; + } + } + } + + if (channelId.equalsIgnoreCase(item.getChannelId())) { + item.setPoster(channel); + break; + } } } @@ -46,7 +162,57 @@ 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 + public void onClick(View view) { + if (listener != null && comment.getPoster() != null) { + listener.onClaimClicked(comment.getPoster()); + } + } + }); + + holder.replyLink.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (replyListener != null) { + replyListener.onReplyClicked(comment); + } + } + }); + } + + public interface ReplyClickListener { + void onReplyClicked(Comment comment); } } \ No newline at end of file diff --git a/app/src/main/java/io/lbry/browser/model/Comment.java b/app/src/main/java/io/lbry/browser/model/Comment.java index c1f6ca72..60c16628 100644 --- a/app/src/main/java/io/lbry/browser/model/Comment.java +++ b/app/src/main/java/io/lbry/browser/model/Comment.java @@ -1,21 +1,48 @@ package io.lbry.browser.model; -import android.util.Log; - import org.json.JSONException; import org.json.JSONObject; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; + +import io.lbry.browser.utils.Helper; import lombok.Data; @Data public class Comment { - private final String channelName, text, id, parentId; + public static final double COST = 0.25; + public static final int MAX_LENGTH = 2000; - public Comment(String channelName, String text, String id, String parentId) { + private Claim poster; + private String claimId; + private List 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/ChannelCommentsFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelCommentsFragment.java new file mode 100644 index 00000000..25012261 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelCommentsFragment.java @@ -0,0 +1,705 @@ +package io.lbry.browser.ui.channel; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatSpinner; +import androidx.core.text.HtmlCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +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.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +import io.lbry.browser.BuildConfig; +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.listener.SdkStatusListener; +import io.lbry.browser.listener.WalletBalanceListener; +import io.lbry.browser.model.Claim; +import io.lbry.browser.model.Comment; +import io.lbry.browser.model.WalletBalance; +import io.lbry.browser.tasks.CommentCreateWithTipTask; +import io.lbry.browser.tasks.CommentListHandler; +import io.lbry.browser.tasks.CommentListTask; +import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask; +import io.lbry.browser.tasks.claim.ClaimListResultHandler; +import io.lbry.browser.tasks.claim.ClaimListTask; +import io.lbry.browser.tasks.claim.ClaimResultHandler; +import io.lbry.browser.tasks.claim.ResolveTask; +import io.lbry.browser.tasks.lbryinc.LogPublishTask; +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 io.lbry.browser.utils.Lbryio; +import lombok.Setter; + +public class ChannelCommentsFragment extends Fragment implements SdkStatusListener, WalletBalanceListener { + + @Setter + private Claim claim; + private CommentListAdapter commentListAdapter; + + private Comment replyToComment; + private View containerReplyToComment; + private TextView textReplyingTo; + private TextView textReplyToBody; + private View buttonClearReplyToComment; + + 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_channel_comments, container, false); + + 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); + 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); + + RecyclerView commentList = root.findViewById(R.id.channel_comments_list); + commentList.setLayoutManager(new LinearLayoutManager(getContext())); + + initCommentForm(root); + setupInlineChannelCreator( + inlineChannelCreator, + inlineChannelCreatorInputName, + inlineChannelCreatorInputDeposit, + inlineChannelCreatorInlineBalance, + inlineChannelCreatorInlineBalanceValue, + inlineChannelCreatorCancelLink, + inlineChannelCreatorCreateButton, + inlineChannelCreatorProgress, + commentChannelSpinner, + commentChannelSpinnerAdapter + ); + + return root; + } + + public void onResume() { + super.onResume(); + Context context = getContext(); + if (!Lbry.SDK_READY) { + if (context instanceof MainActivity) { + ((MainActivity) context).addSdkStatusListener(this); + } + } else { + onSdkReady(); + } + + if (context instanceof MainActivity) { + ((MainActivity) context).addWalletBalanceListener(this); + } + checkCommentSdkInitializing(); + } + + public void onStop() { + super.onStop(); + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.removeSdkStatusListener(this); + activity.removeWalletBalanceListener(this); + } + } + + private void checkCommentSdkInitializing() { + View root = getView(); + if (root != null) { + TextView commentsSDKInitializing = root.findViewById(R.id.channel_comments_sdk_initializing); + Helper.setViewVisibility(commentsSDKInitializing, Lbry.SDK_READY ? View.GONE : View.VISIBLE); + } + } + + @Override + public void onSdkReady() { + fetchChannels(); + checkAndLoadComments(); + } + + private void checkAndLoadComments() { + View root = getView(); + if (root != null) { + checkCommentSdkInitializing(); + RecyclerView commentsList = root.findViewById(R.id.channel_comments_list); + if (commentsList == null || commentsList.getAdapter() == null || commentsList.getAdapter().getItemCount() == 0) { + loadComments(); + } + } + } + + private void loadComments() { + View root = getView(); + ProgressBar relatedLoading = root.findViewById(R.id.channel_comments_progress); + if (claim != null && root != null) { + CommentListTask task = new CommentListTask(1, 500, claim.getClaimId(), relatedLoading, new CommentListHandler() { + @Override + public void onSuccess(List 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/ChannelFormFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java index fc39135c..45e3a9fc 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 @@ -377,13 +377,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/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 9abec9c3..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,8 @@ 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; import android.view.ContextMenu; import android.view.LayoutInflater; @@ -20,10 +21,12 @@ 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; 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,34 @@ 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; + 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 +238,30 @@ 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); + 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() { @@ -444,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); @@ -457,6 +525,7 @@ public class FileViewFragment extends BaseFragment implements loadFile(); } checkOwnClaim(); + fetchChannels(); checkAndLoadComments(); } @@ -500,6 +569,10 @@ public class FileViewFragment extends BaseFragment implements if (!claim.isPlayable() && !claim.isViewable()) { showUnsupportedView(); } + } else { + if (!claim.isPlayable() && !claim.isViewable()) { + restoreMainActionButton(); + } } initialFileLoadDone = true; @@ -586,6 +659,7 @@ public class FileViewFragment extends BaseFragment implements } else { onSdkReady(); } + checkCommentSdkInitializing(); } public void onStop() { @@ -1000,6 +1074,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); @@ -1240,15 +1331,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 } @@ -1336,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) { @@ -1892,32 +1978,80 @@ 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) { + 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); + } + } + }); + commentListAdapter.setReplyListener(new CommentListAdapter.ReplyClickListener() { + @Override + public void onReplyClicked(Comment comment) { + setReplyToComment(comment); + } + }); - 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); + task.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); + } } } @@ -2336,6 +2470,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(); } @@ -2457,4 +2594,296 @@ 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)); + + 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.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(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); + } } 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 d308afba..7bc29401 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 ); } @@ -1274,128 +1278,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..1948ef6d --- /dev/null +++ b/app/src/main/res/layout/container_comment_form.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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/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 7d5fe110..5805f84f 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,7 +81,23 @@ 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. Please select a channel to repost on. + Reply + Replying to %1$s + + 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