Browse Source

View model (#34)

Change TopicActivity to MVVM arch using ViewModel, Add EditPost, Clean-up TopicActivity, Add post focus, Other changes and fixes
pull/44/head
oogee 6 years ago
committed by Apostolof
parent
commit
6f1de802e3
  1. 8
      app/build.gradle
  2. 6
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/Posting.java
  3. 881
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  4. 411
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java
  5. 23
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java
  6. 61
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/DeleteTask.java
  7. 77
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/EditTask.java
  8. 51
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditResult.java
  9. 80
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditTask.java
  10. 91
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReply.java
  11. 34
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReplyResult.java
  12. 80
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/ReplyTask.java
  13. 172
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java
  14. 125
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTaskResult.java
  15. 6
      app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java
  16. 41
      app/src/main/java/gr/thmmy/mthmmy/model/Post.java
  17. 76
      app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java
  18. 18
      app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java
  19. 316
      app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java
  20. 5
      app/src/main/res/drawable/ic_edit_white_24dp.xml
  21. 118
      app/src/main/res/layout/activity_topic_edit_row.xml
  22. 15
      app/src/main/res/layout/activity_topic_overflow_menu.xml
  23. 4
      app/src/main/res/layout/activity_topic_quick_reply_row.xml
  24. 6
      app/src/main/res/values/strings.xml

8
app/build.gradle

@ -25,6 +25,11 @@ android {
archivesBaseName = archivesBaseName + "-$date" archivesBaseName = archivesBaseName + "-$date"
} }
} }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
} }
dependencies { dependencies {
@ -37,7 +42,7 @@ dependencies {
implementation 'com.android.support:cardview-v7:27.1.1' implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1' implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.google.firebase:firebase-core:16.0.1' implementation 'com.google.firebase:firebase-core:16.0.1'
implementation 'com.google.firebase:firebase-messaging:17.0.0' implementation 'com.google.firebase:firebase-messaging:17.1.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4' implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4'
implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.picasso:picasso:2.5.2' implementation 'com.squareup.picasso:picasso:2.5.2'
@ -56,6 +61,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.0' implementation 'com.jakewharton.timber:timber:4.7.0'
implementation 'net.gotev:uploadservice:3.4.2' implementation 'net.gotev:uploadservice:3.4.2'
implementation 'net.gotev:uploadservice-okhttp:3.4.2' implementation 'net.gotev:uploadservice-okhttp:3.4.2'
implementation 'android.arch.lifecycle:extensions:1.1.1'
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

6
app/src/main/java/gr/thmmy/mthmmy/activities/topic/Posting.java

@ -11,11 +11,11 @@ import timber.log.Timber;
/** /**
* This is a utility class containing a collection of static methods to help with topic replying. * This is a utility class containing a collection of static methods to help with topic replying.
*/ */
class Posting { public class Posting {
/** /**
* {@link REPLY_STATUS} enum defines the different possible outcomes of a topic reply request. * {@link REPLY_STATUS} enum defines the different possible outcomes of a topic reply request.
*/ */
enum REPLY_STATUS { public enum REPLY_STATUS {
/** /**
* The request was successful * The request was successful
*/ */
@ -54,7 +54,7 @@ class Posting {
* @return a {@link REPLY_STATUS} that describes the response status * @return a {@link REPLY_STATUS} that describes the response status
* @throws IOException method relies to {@link org.jsoup.Jsoup#parse(String)} * @throws IOException method relies to {@link org.jsoup.Jsoup#parse(String)}
*/ */
static REPLY_STATUS replyStatus(Response response) throws IOException { public static REPLY_STATUS replyStatus(Response response) throws IOException {
if (response.code() == 404) return REPLY_STATUS.NOT_FOUND; if (response.code() == 404) return REPLY_STATUS.NOT_FOUND;
if (response.code() < 200 || response.code() >= 400) return REPLY_STATUS.OTHER_ERROR; if (response.code() < 200 || response.code() >= 400) return REPLY_STATUS.OTHER_ERROR;
String finalUrl = response.request().url().toString(); String finalUrl = response.request().url().toString();

881
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java

File diff suppressed because it is too large

411
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java

@ -2,8 +2,8 @@ package gr.thmmy.mthmmy.activities.topic;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
@ -17,9 +17,7 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.content.res.AppCompatResources; import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.AppCompatImageButton; import android.support.v7.widget.AppCompatImageButton;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -37,7 +35,6 @@ import android.widget.TextView;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -49,6 +46,7 @@ import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.model.ThmmyFile;
import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.utils.CircleTransform; import gr.thmmy.mthmmy.utils.CircleTransform;
import gr.thmmy.mthmmy.viewmodel.TopicViewModel;
import timber.log.Timber; import timber.log.Timber;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@ -71,119 +69,72 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
*/ */
private static int THUMBNAIL_SIZE; private static int THUMBNAIL_SIZE;
private final Context context; private final Context context;
private String topicTitle; private final OnPostFocusChangeListener postFocusListener;
private String baseUrl;
private final ArrayList<Integer> toQuoteList = new ArrayList<>();
private final List<Post> postsList; private final List<Post> postsList;
/** private TopicViewModel viewModel;
* Used to hold the state of visibility and other attributes for views that are animated or
* otherwise changed. Used in combination with {@link #isUserExtraInfoVisibile} and
* {@link #isQuoteButtonChecked}.
*/
private final ArrayList<boolean[]> viewProperties = new ArrayList<>();
/**
* Index of state indicator in the boolean array. If true user's extra info are expanded and
* visible.
*/
private static final int isUserExtraInfoVisibile = 0;
/**
* Index of state indicator in the boolean array. If true quote button for this post is checked.
*/
private static final int isQuoteButtonChecked = 1;
private TopicActivity.TopicTask topicTask;
private TopicActivity.ReplyTask replyTask;
private TopicActivity.DeleteTask deleteTask;
private final int VIEW_TYPE_POST = 0;
private final int VIEW_TYPE_QUICK_REPLY = 1;
private final String[] replyDataHolder = new String[2];
private final int replySubject = 0, replyText = 1;
private String numReplies, seqnum, sc, topic, buildedQuotes;
private boolean canReply = false;
/** /**
* @param context the context of the {@link RecyclerView} * @param context the context of the {@link RecyclerView}
* @param postsList List of {@link Post} objects to use * @param postsList List of {@link Post} objects to use
*/ */
TopicAdapter(Context context, List<Post> postsList, String baseUrl, TopicAdapter(TopicActivity context, List<Post> postsList) {
TopicActivity.TopicTask topicTask) {
this.context = context; this.context = context;
this.postsList = postsList; this.postsList = postsList;
this.baseUrl = baseUrl; this.postFocusListener = context;
THUMBNAIL_SIZE = (int) context.getResources().getDimension(R.dimen.thumbnail_size);
for (int i = 0; i < postsList.size(); ++i) {
//Initializes properties, array's values will be false by default
viewProperties.add(new boolean[3]);
}
this.topicTask = topicTask;
}
ArrayList<Integer> getToQuoteList() {
return toQuoteList;
}
void prepareForReply(TopicActivity.ReplyTask replyTask, String topicTitle, String numReplies, viewModel = ViewModelProviders.of(context).get(TopicViewModel.class);
String seqnum, String sc, String topic, String buildedQuotes) {
this.replyTask = replyTask;
this.topicTitle = topicTitle;
this.numReplies = numReplies;
this.seqnum = seqnum;
this.sc = sc;
this.topic = topic;
this.buildedQuotes = buildedQuotes;
}
void prepareForDelete(TopicActivity.DeleteTask deleteTask) { THUMBNAIL_SIZE = (int) context.getResources().getDimension(R.dimen.thumbnail_size);
this.deleteTask = deleteTask;
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
return postsList.get(position) == null ? VIEW_TYPE_QUICK_REPLY : VIEW_TYPE_POST; return postsList.get(position).getPostType();
} }
@NonNull
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_POST) { if (viewType == Post.TYPE_POST) {
View itemView = LayoutInflater.from(parent.getContext()) View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.activity_topic_post_row, parent, false); .inflate(R.layout.activity_topic_post_row, parent, false);
return new PostViewHolder(itemView); return new PostViewHolder(itemView);
} else if (viewType == VIEW_TYPE_QUICK_REPLY) { } else if (viewType == Post.TYPE_QUICK_REPLY) {
View view = LayoutInflater.from(parent.getContext()). View view = LayoutInflater.from(parent.getContext()).
inflate(R.layout.activity_topic_quick_reply_row, parent, false); inflate(R.layout.activity_topic_quick_reply_row, parent, false);
view.findViewById(R.id.quick_reply_submit).setEnabled(true); view.findViewById(R.id.quick_reply_submit).setEnabled(true);
final EditText quickReplyText = view.findViewById(R.id.quick_reply_text); final EditText quickReplyText = view.findViewById(R.id.quick_reply_text);
quickReplyText.setFocusableInTouchMode(true); quickReplyText.setFocusableInTouchMode(true);
quickReplyText.setOnFocusChangeListener(new View.OnFocusChangeListener() { quickReplyText.setOnFocusChangeListener((v, hasFocus) -> quickReplyText.post(() -> {
@Override InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
public void onFocusChange(View v, boolean hasFocus) { imm.showSoftInput(quickReplyText, InputMethodManager.SHOW_IMPLICIT);
quickReplyText.post(new Runnable() { }));
@Override
public void run() {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(quickReplyText, InputMethodManager.SHOW_IMPLICIT);
}
});
}
});
quickReplyText.requestFocus(); quickReplyText.requestFocus();
//Default post subject return new QuickReplyViewHolder(view);
replyDataHolder[replySubject] = "Re: " + topicTitle; } else if (viewType == Post.TYPE_EDIT) {
//Build quotes View view = LayoutInflater.from(parent.getContext()).
if (!Objects.equals(buildedQuotes, "")) inflate(R.layout.activity_topic_edit_row, parent, false);
replyDataHolder[replyText] = buildedQuotes; view.findViewById(R.id.edit_message_submit).setEnabled(true);
return new QuickReplyViewHolder(view, new CustomEditTextListener(replySubject),
new CustomEditTextListener(replyText)); final EditText editPostEdittext = view.findViewById(R.id.edit_message_text);
editPostEdittext.setFocusableInTouchMode(true);
editPostEdittext.setOnFocusChangeListener((v, hasFocus) -> editPostEdittext.post(() -> {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editPostEdittext, InputMethodManager.SHOW_IMPLICIT);
}));
editPostEdittext.requestFocus();
return new EditMessageViewHolder(view);
} else {
throw new IllegalArgumentException("Unknown view type");
} }
return null;
} }
@SuppressLint({"SetJavaScriptEnabled", "SetTextI18n"}) @SuppressLint({"SetJavaScriptEnabled", "SetTextI18n"})
@Override @Override
public void onBindViewHolder(final RecyclerView.ViewHolder currentHolder, public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder currentHolder,
final int position) { final int position) {
if (currentHolder instanceof PostViewHolder) { if (currentHolder instanceof PostViewHolder) {
final Post currentPost = postsList.get(position); final Post currentPost = postsList.get(position);
@ -245,12 +196,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
attached.setTextColor(filesTextColor); attached.setTextColor(filesTextColor);
attached.setPadding(0, 3, 0, 3); attached.setPadding(0, 3, 0, 3);
attached.setOnClickListener(new View.OnClickListener() { attached.setOnClickListener(view -> ((BaseActivity) context).downloadFile(attachedFile));
@Override
public void onClick(View view) {
((BaseActivity) context).downloadFile(attachedFile);
}
});
holder.postFooter.addView(attached); holder.postFooter.addView(attached);
} }
@ -329,11 +275,11 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
, "fonts/fontawesome-webfont.ttf")); , "fonts/fontawesome-webfont.ttf"));
String aStar = context.getResources().getString(R.string.fa_icon_star); String aStar = context.getResources().getString(R.string.fa_icon_star);
String usersStars = ""; StringBuilder usersStars = new StringBuilder();
for (int i = 0; i < mNumberOfStars; ++i) { for (int i = 0; i < mNumberOfStars; ++i) {
usersStars += aStar; usersStars.append(aStar);
} }
holder.stars.setText(usersStars); holder.stars.setText(usersStars.toString());
holder.stars.setTextColor(mUserColor); holder.stars.setTextColor(mUserColor);
holder.stars.setVisibility(View.VISIBLE); holder.stars.setVisibility(View.VISIBLE);
} else } else
@ -349,7 +295,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} else holder.cardChildLinear.setBackground(null); } else holder.cardChildLinear.setBackground(null);
//Avoid's view's visibility recycling //Avoid's view's visibility recycling
if (!currentPost.isDeleted() && viewProperties.get(position)[isUserExtraInfoVisibile]) { if (!currentPost.isDeleted() && viewModel.isUserExtraInfoVisible(holder.getAdapterPosition())) {
holder.userExtraInfo.setVisibility(View.VISIBLE); holder.userExtraInfo.setVisibility(View.VISIBLE);
holder.userExtraInfo.setAlpha(1.0f); holder.userExtraInfo.setAlpha(1.0f);
@ -372,53 +318,39 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} }
if (!currentPost.isDeleted()) { if (!currentPost.isDeleted()) {
//Sets graphics behavior //Sets graphics behavior
holder.thumbnail.setOnClickListener(new View.OnClickListener() { holder.thumbnail.setOnClickListener(view -> {
@Override //Clicking the thumbnail opens user's profile
public void onClick(View view) { Intent intent = new Intent(context, ProfileActivity.class);
//Clicking the thumbnail opens user's profile Bundle extras = new Bundle();
Intent intent = new Intent(context, ProfileActivity.class); extras.putString(BUNDLE_PROFILE_URL, currentPost.getProfileURL());
Bundle extras = new Bundle(); if (currentPost.getThumbnailURL() == null)
extras.putString(BUNDLE_PROFILE_URL, currentPost.getProfileURL()); extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, "");
if (currentPost.getThumbnailURL() == null) else
extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, currentPost.getThumbnailURL());
else extras.putString(BUNDLE_PROFILE_USERNAME, currentPost.getAuthor());
extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, currentPost.getThumbnailURL()); intent.putExtras(extras);
extras.putString(BUNDLE_PROFILE_USERNAME, currentPost.getAuthor()); intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
intent.putExtras(extras); context.startActivity(intent);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}); });
holder.header.setOnClickListener(new View.OnClickListener() { holder.header.setOnClickListener(v -> {
@Override //Clicking the header makes it expand/collapse
public void onClick(View v) { viewModel.toggleUserInfo(holder.getAdapterPosition());
//Clicking the header makes it expand/collapse TopicAnimations.animateUserExtraInfoVisibility(holder.username,
boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); holder.subject, Color.parseColor("#FFFFFF"),
tmp[isUserExtraInfoVisibile] = !tmp[isUserExtraInfoVisibile]; Color.parseColor("#757575"), holder.userExtraInfo);
viewProperties.set(holder.getAdapterPosition(), tmp);
TopicAnimations.animateUserExtraInfoVisibility(holder.username,
holder.subject, Color.parseColor("#FFFFFF"),
Color.parseColor("#757575"), holder.userExtraInfo);
}
}); });
//Clicking the expanded part of a header (the extra info) makes it collapse //Clicking the expanded part of a header (the extra info) makes it collapse
holder.userExtraInfo.setOnClickListener(new View.OnClickListener() { holder.userExtraInfo.setOnClickListener(v -> {
@Override viewModel.hideUserInfo(holder.getAdapterPosition());
public void onClick(View v) { TopicAnimations.animateUserExtraInfoVisibility(holder.username,
boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); holder.subject, Color.parseColor("#FFFFFF"),
tmp[isUserExtraInfoVisibile] = false; Color.parseColor("#757575"), (LinearLayout) v);
viewProperties.set(holder.getAdapterPosition(), tmp);
TopicAnimations.animateUserExtraInfoVisibility(holder.username,
holder.subject, Color.parseColor("#FFFFFF"),
Color.parseColor("#757575"), (LinearLayout) v);
}
}); });
} else { } else {
holder.header.setOnClickListener(null); holder.header.setOnClickListener(null);
holder.userExtraInfo.setOnClickListener(null); holder.userExtraInfo.setOnClickListener(null);
} }
holder.overflowButton.setOnClickListener(new View.OnClickListener() { holder.overflowButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
@ -449,8 +381,20 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
popUp.dismiss(); popUp.dismiss();
} }
}); });
}
final TextView editPostButton = popUpContent.findViewById(R.id.edit_post);
TextView deletePostButton = popUpContent.findViewById(R.id.delete_post); if (viewModel.isEditingPost() || currentPost.getPostEditURL() == null || currentPost.getPostEditURL().equals("")) {
editPostButton.setVisibility(View.GONE);
} else {
editPostButton.setOnClickListener(v -> {
viewModel.prepareForEdit(position, postsList.get(position).getPostEditURL());
popUp.dismiss();
});
}
TextView deletePostButton = popUpContent.findViewById(R.id.delete_post);
if (currentPost.getPostDeleteURL() == null || currentPost.getPostDeleteURL().equals("")) { if (currentPost.getPostDeleteURL() == null || currentPost.getPostDeleteURL().equals("")) {
deletePostButton.setVisibility(View.GONE); deletePostButton.setVisibility(View.GONE);
@ -477,37 +421,25 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
}); });
} }
//Displays the popup //Displays the popup
popUp.showAsDropDown(holder.overflowButton); popUp.showAsDropDown(holder.overflowButton);
}
}); });
//noinspection PointlessBooleanExpression,ConstantConditions //noinspection PointlessBooleanExpression,ConstantConditions
if (!BaseActivity.getSessionManager().isLoggedIn() || !canReply) { if (!BaseActivity.getSessionManager().isLoggedIn() || !viewModel.canReply()) {
holder.quoteToggle.setVisibility(View.GONE); holder.quoteToggle.setVisibility(View.GONE);
} else { } else {
if (viewProperties.get(position)[isQuoteButtonChecked]) if (viewModel.getToQuoteList().contains(currentPost.getPostIndex()))
holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked_accent_24dp); holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked_accent_24dp);
else else
holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked_grey_24dp); holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked_grey_24dp);
//Sets graphics behavior //Sets graphics behavior
holder.quoteToggle.setOnClickListener(new View.OnClickListener() { holder.quoteToggle.setOnClickListener(view -> {
@Override viewModel.postIndexToggle(currentPost.getPostIndex());
public void onClick(View view) { if (viewModel.getToQuoteList().contains(currentPost.getPostIndex()))
boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked_accent_24dp);
if (tmp[isQuoteButtonChecked]) { else
if (toQuoteList.contains(postsList.indexOf(currentPost))) { holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked_grey_24dp);
toQuoteList.remove(toQuoteList.indexOf(postsList.indexOf(currentPost)));
} else
Timber.i("An error occurred while trying to exclude post fromtoQuoteList, post wasn't there!");
holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked_grey_24dp);
} else {
toQuoteList.add(postsList.indexOf(currentPost));
holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked_accent_24dp);
}
tmp[isQuoteButtonChecked] = !tmp[isQuoteButtonChecked];
viewProperties.set(holder.getAdapterPosition(), tmp);
}
}); });
} }
} else if (currentHolder instanceof QuickReplyViewHolder) { } else if (currentHolder instanceof QuickReplyViewHolder) {
@ -525,41 +457,65 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
.transform(new CircleTransform()) .transform(new CircleTransform())
.into(holder.thumbnail); .into(holder.thumbnail);
holder.username.setText(getSessionManager().getUsername()); holder.username.setText(getSessionManager().getUsername());
holder.quickReplySubject.setText(replyDataHolder[replySubject]); holder.quickReplySubject.setText("Re: " + viewModel.getTopicTitle());
if (replyDataHolder[replyText] != null && !Objects.equals(replyDataHolder[replyText], "")) holder.quickReply.setText(viewModel.getBuildedQuotes());
holder.quickReply.setText(replyDataHolder[replyText]);
holder.submitButton.setOnClickListener(new View.OnClickListener() {
@Override holder.submitButton.setOnClickListener(view -> {
public void onClick(View view) { if (holder.quickReplySubject.getText().toString().isEmpty()) return;
if (holder.quickReplySubject.getText().toString().isEmpty()) return; if (holder.quickReply.getText().toString().isEmpty()) return;
if (holder.quickReply.getText().toString().isEmpty()) return; holder.submitButton.setEnabled(false);
holder.submitButton.setEnabled(false);
replyTask.execute(holder.quickReplySubject.getText().toString(), viewModel.postReply(context, holder.quickReplySubject.getText().toString(),
holder.quickReply.getText().toString(), numReplies, seqnum, sc, topic); holder.quickReply.getText().toString());
holder.quickReplySubject.getText().clear(); holder.quickReplySubject.getText().clear();
holder.quickReplySubject.setText("Re: " + topicTitle); holder.quickReplySubject.setText("Re: " + viewModel.getTopicTitle());
holder.quickReply.getText().clear(); holder.quickReply.getText().clear();
holder.submitButton.setEnabled(true); holder.submitButton.setEnabled(true);
}
}); });
if (backPressHidden) { if (backPressHidden) {
holder.quickReply.requestFocus(); holder.quickReply.requestFocus();
backPressHidden = false; backPressHidden = false;
} }
} } else if (currentHolder instanceof EditMessageViewHolder) {
} final EditMessageViewHolder holder = (EditMessageViewHolder) currentHolder;
//noinspection ConstantConditions
Picasso.with(context)
.load(getSessionManager().getAvatarLink())
.resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
.centerCrop()
.error(ResourcesCompat.getDrawable(context.getResources()
, R.drawable.ic_default_user_thumbnail_white_24dp, null))
.placeholder(ResourcesCompat.getDrawable(context.getResources()
, R.drawable.ic_default_user_thumbnail_white_24dp, null))
.transform(new CircleTransform())
.into(holder.thumbnail);
holder.username.setText(getSessionManager().getUsername());
holder.editSubject.setText(postsList.get(position).getSubject());
holder.editMessage.setText(viewModel.getPostBeingEditedText());
holder.submitButton.setOnClickListener(view -> {
if (holder.editSubject.getText().toString().isEmpty()) return;
if (holder.editMessage.getText().toString().isEmpty()) return;
holder.submitButton.setEnabled(false);
viewModel.editPost(position, holder.editSubject.getText().toString(), holder.editMessage.getText().toString());
holder.editSubject.getText().clear();
holder.editSubject.setText(postsList.get(position).getSubject());
holder.submitButton.setEnabled(true);
});
void resetTopic(String baseUrl, TopicActivity.TopicTask topicTask, boolean canReply) { if (backPressHidden) {
this.baseUrl = baseUrl; holder.editMessage.requestFocus();
this.topicTask = topicTask; backPressHidden = false;
this.canReply = canReply; }
viewProperties.clear();
for (int i = 0; i < postsList.size(); ++i) {
//Initializes properties, array's values will be false by default
viewProperties.add(new boolean[3]);
} }
} }
@ -629,19 +585,33 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final EditText quickReply, quickReplySubject; final EditText quickReply, quickReplySubject;
final AppCompatImageButton submitButton; final AppCompatImageButton submitButton;
QuickReplyViewHolder(View quickReply, CustomEditTextListener replySubject QuickReplyViewHolder(View quickReply) {
, CustomEditTextListener replyText) {
super(quickReply); super(quickReply);
thumbnail = quickReply.findViewById(R.id.thumbnail); thumbnail = quickReply.findViewById(R.id.thumbnail);
username = quickReply.findViewById(R.id.username); username = quickReply.findViewById(R.id.username);
this.quickReply = quickReply.findViewById(R.id.quick_reply_text); this.quickReply = quickReply.findViewById(R.id.quick_reply_text);
this.quickReply.addTextChangedListener(replyText);
quickReplySubject = quickReply.findViewById(R.id.quick_reply_subject); quickReplySubject = quickReply.findViewById(R.id.quick_reply_subject);
quickReplySubject.addTextChangedListener(replySubject);
submitButton = quickReply.findViewById(R.id.quick_reply_submit); submitButton = quickReply.findViewById(R.id.quick_reply_submit);
} }
} }
private static class EditMessageViewHolder extends RecyclerView.ViewHolder {
final ImageView thumbnail;
final TextView username;
final EditText editMessage, editSubject;
final AppCompatImageButton submitButton;
EditMessageViewHolder(View editView) {
super(editView);
thumbnail = editView.findViewById(R.id.thumbnail);
username = editView.findViewById(R.id.username);
editMessage = editView.findViewById(R.id.edit_message_text);
editSubject = editView.findViewById(R.id.edit_message_subject);
submitButton = editView.findViewById(R.id.edit_message_submit);
}
}
/** /**
* This class is used to handle link clicks in WebViews. When link url is one that the app can * This class is used to handle link clicks in WebViews. When link url is one that the app can
* handle internally, it does. Otherwise user is prompt to open the link in a browser. * handle internally, it does. Otherwise user is prompt to open the link in a browser.
@ -667,26 +637,41 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final String uriString = uri.toString(); final String uriString = uri.toString();
ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(uri); ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(uri);
viewModel.stopLoading();
if (target.is(ThmmyPage.PageCategory.TOPIC)) { if (target.is(ThmmyPage.PageCategory.TOPIC)) {
//This url points to a topic //This url points to a topic
//Checks if this is the current topic //Checks if the page to be loaded is the one already shown
if (Objects.equals(uriString.substring(0, uriString.lastIndexOf(".")), baseUrl)) { if (uriString.contains(viewModel.getBaseUrl())) {
//Gets uri's targeted message's index number Timber.e("reached here!");
String msgIndexReq = uriString.substring(uriString.indexOf("msg") + 3); if (uriString.contains("topicseen#new") || uriString.contains("#new")) {
if (msgIndexReq.contains("#")) if (viewModel.getCurrentPageIndex() == viewModel.getPageCount()) {
msgIndexReq = msgIndexReq.substring(0, msgIndexReq.indexOf("#")); //same page
else postFocusListener.onPostFocusChange(getItemCount() - 1);
msgIndexReq = msgIndexReq.substring(0, msgIndexReq.indexOf(";")); Timber.e("new");
//Checks if this post is in the current topic's page
for (Post post : postsList) {
if (post.getPostIndex() == Integer.parseInt(msgIndexReq)) {
// TODO Don't restart Activity, Just change post focus
return true; return true;
} }
} }
if (uriString.contains("msg")) {
topicTask.execute(uri.toString()); String tmpUrlSbstr = uriString.substring(uriString.indexOf("msg") + 3);
if (tmpUrlSbstr.contains("msg"))
tmpUrlSbstr = tmpUrlSbstr.substring(0, tmpUrlSbstr.indexOf("msg") - 1);
int testAgainst = Integer.parseInt(tmpUrlSbstr);
Timber.e("reached tthere! %s", testAgainst);
for (int i = 0; i < postsList.size(); i++) {
if (postsList.get(i).getPostIndex() == testAgainst) {
//same page
Timber.e(Integer.toString(i));
postFocusListener.onPostFocusChange(i);
return true;
}
}
} else if ((Objects.equals(uriString, viewModel.getBaseUrl()) && viewModel.getCurrentPageIndex() == 1) ||
Integer.parseInt(uriString.substring(viewModel.getBaseUrl().length() + 1)) / 15 + 1 ==
viewModel.getCurrentPageIndex()) {
//same page
Timber.e("ha");
return true;
}
} }
Intent intent = new Intent(context, TopicActivity.class); Intent intent = new Intent(context, TopicActivity.class);
@ -727,25 +712,9 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} }
private class CustomEditTextListener implements TextWatcher { //we need to set a callback to topic activity to scroll the recyclerview when post focus is requested
private final int positionInDataHolder; public interface OnPostFocusChangeListener {
void onPostFocusChange(int position);
CustomEditTextListener(int positionInDataHolder) {
this.positionInDataHolder = positionInDataHolder;
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
replyDataHolder[positionInDataHolder] = charSequence.toString();
}
@Override
public void afterTextChanged(Editable editable) {
}
} }
/** /**

23
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java

@ -28,7 +28,7 @@ import timber.log.Timber;
* <li>{@link #parseTopicNumberOfPages(Document, int, ParseHelpers.Language)}</li> * <li>{@link #parseTopicNumberOfPages(Document, int, ParseHelpers.Language)}</li>
* <li>{@link #parseTopic(Document, ParseHelpers.Language)}</li> * <li>{@link #parseTopic(Document, ParseHelpers.Language)}</li>
*/ */
class TopicParser { public class TopicParser {
//User colors //User colors
private static final int USER_COLOR_BLACK = Color.parseColor("#000000"); private static final int USER_COLOR_BLACK = Color.parseColor("#000000");
private static final int USER_COLOR_RED = Color.parseColor("#F44336"); private static final int USER_COLOR_RED = Color.parseColor("#F44336");
@ -48,7 +48,7 @@ class TopicParser {
* @return String containing html with the usernames of users * @return String containing html with the usernames of users
* @see org.jsoup.Jsoup Jsoup * @see org.jsoup.Jsoup Jsoup
*/ */
static String parseUsersViewingThisTopic(Document topic, ParseHelpers.Language language) { public static String parseUsersViewingThisTopic(Document topic, ParseHelpers.Language language) {
if (language.is(ParseHelpers.Language.GREEK)) if (language.is(ParseHelpers.Language.GREEK))
return topic.select("td:containsOwn(διαβάζουν αυτό το θέμα)").first().html(); return topic.select("td:containsOwn(διαβάζουν αυτό το θέμα)").first().html();
return topic.select("td:containsOwn(are viewing this topic)").first().html(); return topic.select("td:containsOwn(are viewing this topic)").first().html();
@ -64,7 +64,7 @@ class TopicParser {
* @return int containing parsed topic's current page * @return int containing parsed topic's current page
* @see org.jsoup.Jsoup Jsoup * @see org.jsoup.Jsoup Jsoup
*/ */
static int parseCurrentPageIndex(Document topic, ParseHelpers.Language language) { public static int parseCurrentPageIndex(Document topic, ParseHelpers.Language language) {
int parsedPage = 1; int parsedPage = 1;
if (language.is(ParseHelpers.Language.GREEK)) { if (language.is(ParseHelpers.Language.GREEK)) {
@ -102,7 +102,7 @@ class TopicParser {
* @return int containing the number of pages * @return int containing the number of pages
* @see org.jsoup.Jsoup Jsoup * @see org.jsoup.Jsoup Jsoup
*/ */
static int parseTopicNumberOfPages(Document topic, int currentPage, ParseHelpers.Language language) { public static int parseTopicNumberOfPages(Document topic, int currentPage, ParseHelpers.Language language) {
int returnPages = 1; int returnPages = 1;
if (language.is(ParseHelpers.Language.GREEK)) { if (language.is(ParseHelpers.Language.GREEK)) {
@ -140,7 +140,7 @@ class TopicParser {
* @return {@link ArrayList} of {@link Post}s * @return {@link ArrayList} of {@link Post}s
* @see org.jsoup.Jsoup Jsoup * @see org.jsoup.Jsoup Jsoup
*/ */
static ArrayList<Post> parseTopic(Document topic, ParseHelpers.Language language) { public static ArrayList<Post> parseTopic(Document topic, ParseHelpers.Language language) {
//Method's variables //Method's variables
final int NO_INDEX = -1; final int NO_INDEX = -1;
ArrayList<Post> parsedPostsList = new ArrayList<>(); ArrayList<Post> parsedPostsList = new ArrayList<>();
@ -157,7 +157,7 @@ class TopicParser {
//Variables for Post constructor //Variables for Post constructor
String p_userName, p_thumbnailURL, p_subject, p_post, p_postDate, p_profileURL, p_rank, String p_userName, p_thumbnailURL, p_subject, p_post, p_postDate, p_profileURL, p_rank,
p_specialRank, p_gender, p_personalText, p_numberOfPosts, p_postLastEditDate, p_specialRank, p_gender, p_personalText, p_numberOfPosts, p_postLastEditDate,
p_postURL, p_deletePostURL; p_postURL, p_deletePostURL, p_editPostURL;
int p_postNum, p_postIndex, p_numberOfStars, p_userColor; int p_postNum, p_postIndex, p_numberOfStars, p_userColor;
boolean p_isDeleted = false; boolean p_isDeleted = false;
ArrayList<ThmmyFile> p_attachedFiles; ArrayList<ThmmyFile> p_attachedFiles;
@ -174,6 +174,7 @@ class TopicParser {
p_attachedFiles = new ArrayList<>(); p_attachedFiles = new ArrayList<>();
p_postLastEditDate = null; p_postLastEditDate = null;
p_deletePostURL = null; p_deletePostURL = null;
p_editPostURL = null;
//Language independent parsing //Language independent parsing
//Finds thumbnail url //Finds thumbnail url
@ -306,6 +307,12 @@ class TopicParser {
p_deletePostURL = postDelete.attr("href"); p_deletePostURL = postDelete.attr("href");
} }
//Finds post modify url
Element postEdit = thisRow.select("a:has(img[alt='Modify message'])").first();
if (postEdit != null) {
p_editPostURL = postEdit.attr("href");
}
//Finds post's submit date //Finds post's submit date
Element postDate = thisRow.select("div.smalltext:matches(on:)").first(); Element postDate = thisRow.select("div.smalltext:matches(on:)").first();
p_postDate = postDate.text(); p_postDate = postDate.text();
@ -431,13 +438,13 @@ class TopicParser {
parsedPostsList.add(new Post(p_thumbnailURL, p_userName, p_subject, p_post, p_postIndex parsedPostsList.add(new Post(p_thumbnailURL, p_userName, p_subject, p_post, p_postIndex
, p_postNum, p_postDate, p_profileURL, p_rank, p_specialRank, p_gender , p_postNum, p_postDate, p_profileURL, p_rank, p_specialRank, p_gender
, p_numberOfPosts, p_personalText, p_numberOfStars, p_userColor , p_numberOfPosts, p_personalText, p_numberOfStars, p_userColor
, p_attachedFiles, p_postLastEditDate, p_postURL, p_deletePostURL)); , p_attachedFiles, p_postLastEditDate, p_postURL, p_deletePostURL, p_editPostURL, Post.TYPE_POST));
} else { //Deleted user } else { //Deleted user
//Add new post in postsList, only standard information needed //Add new post in postsList, only standard information needed
parsedPostsList.add(new Post(p_thumbnailURL, p_userName, p_subject, p_post parsedPostsList.add(new Post(p_thumbnailURL, p_userName, p_subject, p_post
, p_postIndex , p_postNum, p_postDate, p_userColor, p_attachedFiles , p_postIndex , p_postNum, p_postDate, p_userColor, p_attachedFiles
, p_postLastEditDate, p_postURL, p_deletePostURL)); , p_postLastEditDate, p_postURL, p_deletePostURL, p_editPostURL, Post.TYPE_POST));
} }
} }
return parsedPostsList; return parsedPostsList;

61
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/DeleteTask.java

@ -0,0 +1,61 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import java.io.IOException;
import gr.thmmy.mthmmy.activities.topic.Posting;
import gr.thmmy.mthmmy.base.BaseApplication;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
public class DeleteTask extends AsyncTask<String, Void, Boolean> {
private DeleteTaskCallbacks listener;
public DeleteTask(DeleteTaskCallbacks listener) {
this.listener = listener;
}
@Override
protected void onPreExecute() {
listener.onDeleteTaskStarted();
}
@Override
protected Boolean doInBackground(String... args) {
Request delete = new Request.Builder()
.url(args[0])
.header("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36")
.build();
try {
OkHttpClient client = BaseApplication.getInstance().getClient();
client.newCall(delete).execute();
Response response = client.newCall(delete).execute();
//Response response = client.newCall(delete).execute();
switch (Posting.replyStatus(response)) {
case SUCCESSFUL:
return true;
default:
Timber.e("Something went wrong. Request string: %s", delete.toString());
return true;
}
} catch (IOException e) {
Timber.e(e, "Delete failed.");
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
listener.onDeleteTaskFinished(result);
}
public interface DeleteTaskCallbacks {
void onDeleteTaskStarted();
void onDeleteTaskFinished(boolean result);
}
}

77
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/EditTask.java

@ -0,0 +1,77 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import java.io.IOException;
import gr.thmmy.mthmmy.base.BaseApplication;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.topic.Posting.replyStatus;
public class EditTask extends AsyncTask<String, Void, Boolean> {
private EditTaskCallbacks listener;
private int position;
public EditTask(EditTaskCallbacks listener, int position) {
this.listener = listener;
this.position = position;
}
@Override
protected void onPreExecute() {
listener.onEditTaskStarted();
}
@Override
protected Boolean doInBackground(String... strings) {
RequestBody postBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("message", strings[1])
.addFormDataPart("num_replies", strings[2])
.addFormDataPart("seqnum", strings[3])
.addFormDataPart("sc", strings[4])
.addFormDataPart("subject", strings[5])
.addFormDataPart("topic", strings[6])
.build();
Request post = new Request.Builder()
.url(strings[0])
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36")
.post(postBody)
.build();
try {
OkHttpClient client = BaseApplication.getInstance().getClient();
client.newCall(post).execute();
Response response = client.newCall(post).execute();
switch (replyStatus(response)) {
case SUCCESSFUL:
return true;
case NEW_REPLY_WHILE_POSTING:
//TODO this...
return true;
default:
Timber.e("Malformed post. Request string: %s", post.toString());
return true;
}
} catch (IOException e) {
Timber.e(e, "Edit failed.");
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
listener.onEditTaskFinished(result, position);
}
public interface EditTaskCallbacks {
void onEditTaskStarted();
void onEditTaskFinished(boolean result, int position);
}
}

51
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditResult.java

@ -0,0 +1,51 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
public class PrepareForEditResult {
private final String postText, commitEditUrl, numReplies, seqnum, sc, topic;
private int position;
private boolean successful;
public PrepareForEditResult(String postText, String commitEditUrl, String numReplies, String seqnum,
String sc, String topic, int position, boolean successful) {
this.postText = postText;
this.commitEditUrl = commitEditUrl;
this.numReplies = numReplies;
this.seqnum = seqnum;
this.sc = sc;
this.topic = topic;
this.position = position;
this.successful = successful;
}
public String getPostText() {
return postText;
}
public String getCommitEditUrl() {
return commitEditUrl;
}
public String getNumReplies() {
return numReplies;
}
public String getSeqnum() {
return seqnum;
}
public String getSc() {
return sc;
}
public String getTopic() {
return topic;
}
public int getPosition() {
return position;
}
public boolean isSuccessful() {
return successful;
}
}

80
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditTask.java

@ -0,0 +1,80 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Selector;
import java.io.IOException;
import gr.thmmy.mthmmy.activities.topic.tasks.PrepareForEditResult;
import gr.thmmy.mthmmy.base.BaseApplication;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
public class PrepareForEditTask extends AsyncTask<String, Void, PrepareForEditResult> {
private int position;
private String replyPageUrl;
private PrepareForEditCallbacks listener;
private OnPrepareEditFinished finishListener;
public PrepareForEditTask(PrepareForEditCallbacks listener, OnPrepareEditFinished finishListener, int position, String replyPageUrl) {
this.listener = listener;
this.finishListener = finishListener;
this.position = position;
this.replyPageUrl = replyPageUrl;
}
@Override
protected void onPreExecute() {
listener.onPrepareEditStarted();
}
@Override
protected PrepareForEditResult doInBackground(String... strings) {
Document document;
String url = strings[0];
Request request = new Request.Builder()
.url(url + ";wap2")
.build();
try {
String postText, commitEditURL, numReplies, seqnum, sc, topic;
OkHttpClient client = BaseApplication.getInstance().getClient();
Response response = client.newCall(request).execute();
document = Jsoup.parse(response.body().string());
Element message = document.select("textarea").first();
postText = message.text();
commitEditURL = document.select("form").first().attr("action");
numReplies = replyPageUrl.substring(replyPageUrl.indexOf("num_replies=") + 12);
seqnum = document.select("input[name=seqnum]").first().attr("value");
sc = document.select("input[name=sc]").first().attr("value");
topic = document.select("input[name=topic]").first().attr("value");
return new PrepareForEditResult(postText, commitEditURL, numReplies, seqnum, sc, topic, position, true);
} catch (IOException | Selector.SelectorParseException e) {
Timber.e(e, "Prepare failed.");
return new PrepareForEditResult(null, null, null, null, null, null, position, false);
}
}
@Override
protected void onPostExecute(PrepareForEditResult result) {
finishListener.onPrepareEditFinished(result, position);
}
public interface PrepareForEditCallbacks {
void onPrepareEditStarted();
void onPrepareEditCancelled();
}
public interface OnPrepareEditFinished {
void onPrepareEditFinished(PrepareForEditResult result, int position);
}
}

91
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReply.java

@ -0,0 +1,91 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Selector;
import java.io.IOException;
import gr.thmmy.mthmmy.base.BaseApplication;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
public class PrepareForReply extends AsyncTask<Integer, Void, PrepareForReplyResult> {
private PrepareForReplyCallbacks listener;
private OnPrepareForReplyFinished finishListener;
private String replyPageUrl;
public PrepareForReply(PrepareForReplyCallbacks listener, OnPrepareForReplyFinished finishListener,
String replyPageUrl) {
this.listener = listener;
this.finishListener = finishListener;
this.replyPageUrl = replyPageUrl;
}
@Override
protected void onPreExecute() {
listener.onPrepareForReplyStarted();
}
@Override
protected PrepareForReplyResult doInBackground(Integer... postIndices) {
String numReplies = null;
String seqnum = null;
String sc = null;
String topic = null;
StringBuilder buildedQuotes = new StringBuilder("");
Document document;
Request request = new Request.Builder()
.url(replyPageUrl + ";wap2")
.build();
OkHttpClient client = BaseApplication.getInstance().getClient();
try {
Response response = client.newCall(request).execute();
document = Jsoup.parse(response.body().string());
numReplies = replyPageUrl.substring(replyPageUrl.indexOf("num_replies=") + 12);
seqnum = document.select("input[name=seqnum]").first().attr("value");
sc = document.select("input[name=sc]").first().attr("value");
topic = document.select("input[name=topic]").first().attr("value");
} catch (IOException | Selector.SelectorParseException e) {
Timber.e(e, "Prepare failed.");
}
for (Integer postIndex : postIndices) {
request = new Request.Builder()
.url("https://www.thmmy.gr/smf/index.php?action=quotefast;quote=" +
postIndex + ";" + "sesc=" + sc + ";xml")
.build();
try {
Response response = client.newCall(request).execute();
String body = response.body().string();
buildedQuotes.append(body.substring(body.indexOf("<quote>") + 7, body.indexOf("</quote>")));
buildedQuotes.append("\n\n");
} catch (IOException | Selector.SelectorParseException e) {
Timber.e(e, "Quote building failed.");
}
}
return new PrepareForReplyResult(numReplies, seqnum, sc, topic, buildedQuotes.toString());
}
@Override
protected void onPostExecute(PrepareForReplyResult result) {
finishListener.onPrepareForReplyFinished(result);
}
public interface PrepareForReplyCallbacks {
void onPrepareForReplyStarted();
void onPrepareForReplyCancelled();
}
public interface OnPrepareForReplyFinished {
void onPrepareForReplyFinished(PrepareForReplyResult result);
}
}

34
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReplyResult.java

@ -0,0 +1,34 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
public class PrepareForReplyResult {
private final String numReplies, seqnum, sc, topic, buildedQuotes;
public PrepareForReplyResult(String numReplies, String seqnum, String sc, String topic, String buildedQuotes) {
this.numReplies = numReplies;
this.seqnum = seqnum;
this.sc = sc;
this.topic = topic;
this.buildedQuotes = buildedQuotes;
}
public String getNumReplies() {
return numReplies;
}
public String getSeqnum() {
return seqnum;
}
public String getSc() {
return sc;
}
public String getTopic() {
return topic;
}
public String getBuildedQuotes() {
return buildedQuotes;
}
}

80
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/ReplyTask.java

@ -0,0 +1,80 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import java.io.IOException;
import gr.thmmy.mthmmy.base.BaseApplication;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.topic.Posting.replyStatus;
public class ReplyTask extends AsyncTask<String, Void, Boolean> {
private ReplyTaskCallbacks listener;
private boolean includeAppSignature;
public ReplyTask(ReplyTaskCallbacks listener, boolean includeAppSignature) {
this.listener = listener;
this.includeAppSignature = includeAppSignature;
}
@Override
protected void onPreExecute() {
listener.onReplyTaskStarted();
}
@Override
protected Boolean doInBackground(String... args) {
final String sentFrommTHMMY = includeAppSignature
? "\n[right][size=7pt][i]sent from [url=https://play.google.com/store/apps/details?id=gr.thmmy.mthmmy]mTHMMY [/url][/i][/size][/right]"
: "";
RequestBody postBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("message", args[1] + sentFrommTHMMY)
.addFormDataPart("num_replies", args[2])
.addFormDataPart("seqnum", args[3])
.addFormDataPart("sc", args[4])
.addFormDataPart("subject", args[0])
.addFormDataPart("topic", args[5])
.build();
Request post = new Request.Builder()
.url("https://www.thmmy.gr/smf/index.php?action=post2")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36")
.post(postBody)
.build();
try {
OkHttpClient client = BaseApplication.getInstance().getClient();
client.newCall(post).execute();
Response response = client.newCall(post).execute();
switch (replyStatus(response)) {
case SUCCESSFUL:
return true;
case NEW_REPLY_WHILE_POSTING:
//TODO this...
return true;
default:
Timber.e("Malformed post. Request string: %s", post.toString());
return true;
}
} catch (IOException e) {
Timber.e(e, "Post failed.");
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
listener.onReplyTaskFinished(result);
}
public interface ReplyTaskCallbacks {
void onReplyTaskStarted();
void onReplyTaskFinished(boolean result);
}
}

172
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java

@ -0,0 +1,172 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask;
import android.util.SparseArray;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;
import gr.thmmy.mthmmy.activities.topic.TopicParser;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
/**
* An {@link AsyncTask} that handles asynchronous fetching of this topic page and parsing of its
* data.
* <p>TopicTask's {@link AsyncTask#execute execute} method needs a topic's url as String
* parameter.</p>
*/
public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
private TopicTaskObserver topicTaskObserver;
private OnTopicTaskCompleted finishListener;
public TopicTask(TopicTaskObserver topicTaskObserver, OnTopicTaskCompleted finishListener) {
this.topicTaskObserver = topicTaskObserver;
this.finishListener = finishListener;
}
@Override
protected void onPreExecute() {
topicTaskObserver.onTopicTaskStarted();
}
@Override
protected TopicTaskResult doInBackground(String... strings) {
String topicTitle = null;
String topicTreeAndMods = "";
String topicViewers = "";
ArrayList<Post> newPostsList = null;
int loadedPageTopicId = -1;
int focusedPostIndex = 0;
SparseArray<String> pagesUrls = new SparseArray<>();
int currentPageIndex = 1;
int pageCount = 1;
String baseUrl = "";
String lastPageLoadAttemptedUrl = "";
Document topic = null;
String newPageUrl = strings[0];
//Finds the index of message focus if present
int postFocus = 0;
{
if (newPageUrl.contains("msg")) {
String tmp = newPageUrl.substring(newPageUrl.indexOf("msg") + 3);
if (tmp.contains(";"))
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(";")));
else if (tmp.contains("#"))
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf("#")));
}
}
lastPageLoadAttemptedUrl = newPageUrl;
if (strings[0].substring(0, strings[0].lastIndexOf(".")).contains("topic="))
baseUrl = strings[0].substring(0, strings[0].lastIndexOf(".")); //New topic's base url
String replyPageUrl = null;
Request request = new Request.Builder()
.url(newPageUrl)
.build();
ResultCode resultCode;
try {
Response response = BaseApplication.getInstance().getClient().newCall(request).execute();
topic = Jsoup.parse(response.body().string());
ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic);
//Finds topic's tree, mods and users viewing
topicTreeAndMods = topic.select("div.nav").first().html();
topicViewers = TopicParser.parseUsersViewingThisTopic(topic, language);
//Finds reply page url
Element replyButton = topic.select("a:has(img[alt=Reply])").first();
if (replyButton == null)
replyButton = topic.select("a:has(img[alt=Απάντηση])").first();
if (replyButton != null) replyPageUrl = replyButton.attr("href");
//Finds topic title if missing
topicTitle = topic.select("td[id=top_subject]").first().text();
if (topicTitle.contains("Topic:")) {
topicTitle = topicTitle.substring(topicTitle.indexOf("Topic:") + 7
, topicTitle.indexOf("(Read") - 2);
} else {
topicTitle = topicTitle.substring(topicTitle.indexOf("Θέμα:") + 6
, topicTitle.indexOf("(Αναγνώστηκε") - 2);
Timber.d("Parsed title: %s", topicTitle);
}
//Finds current page's index
currentPageIndex = TopicParser.parseCurrentPageIndex(topic, language);
//Finds number of pages
pageCount = TopicParser.parseTopicNumberOfPages(topic, currentPageIndex, language);
for (int i = 0; i < pageCount; i++) {
//Generate each page's url from topic's base url +".15*numberOfPage"
pagesUrls.put(i, baseUrl + "." + String.valueOf(i * 15));
}
newPostsList = TopicParser.parseTopic(topic, language);
loadedPageTopicId = Integer.parseInt(ThmmyPage.getTopicId(lastPageLoadAttemptedUrl));
//Finds the position of the focused message if present
for (int i = 0; i < newPostsList.size(); ++i) {
if (newPostsList.get(i).getPostIndex() == postFocus) {
focusedPostIndex = i;
break;
}
}
resultCode = ResultCode.SUCCESS;
} catch (IOException e) {
Timber.i(e, "IO Exception");
resultCode = ResultCode.NETWORK_ERROR;
} catch (Exception e) {
if (isUnauthorized(topic)) {
resultCode = ResultCode.UNAUTHORIZED;
} else {
Timber.e(e, "Parsing Error");
resultCode = ResultCode.PARSING_ERROR;
}
}
return new TopicTaskResult(resultCode, baseUrl, topicTitle, replyPageUrl, newPostsList,
loadedPageTopicId, currentPageIndex, pageCount, focusedPostIndex, topicTreeAndMods,
topicViewers, lastPageLoadAttemptedUrl, pagesUrls);
}
private boolean isUnauthorized(Document document) {
return document != null && document.select("body:contains(The topic or board you" +
" are looking for appears to be either missing or off limits to you.)," +
"body:contains(Το θέμα ή πίνακας που ψάχνετε ή δεν υπάρχει ή δεν " +
"είναι προσβάσιμο από εσάς.)").size() > 0;
}
@Override
protected void onPostExecute(TopicTaskResult topicTaskResult) {
finishListener.onTopicTaskCompleted(topicTaskResult);
}
public enum ResultCode {
SUCCESS, NETWORK_ERROR, PARSING_ERROR, UNAUTHORIZED
}
public interface TopicTaskObserver {
void onTopicTaskStarted();
void onTopicTaskCancelled();
}
public interface OnTopicTaskCompleted {
void onTopicTaskCompleted(TopicTaskResult result);
}
}

125
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTaskResult.java

@ -0,0 +1,125 @@
package gr.thmmy.mthmmy.activities.topic.tasks;
import android.util.SparseArray;
import java.util.ArrayList;
import gr.thmmy.mthmmy.activities.topic.tasks.TopicTask;
import gr.thmmy.mthmmy.model.Post;
public class TopicTaskResult {
private final TopicTask.ResultCode resultCode;
/**
* Holds this topic's base url. For example a topic with url similar to
* "https://www.thmmy.gr/smf/index.php?topic=1.15;topicseen" or
* "https://www.thmmy.gr/smf/index.php?topic=1.msg1#msg1"
* has the base url "https://www.thmmy.gr/smf/index.php?topic=1"
*/
private final String baseUrl;
/**
* Holds this topic's title. At first this gets the value of the topic title that came with
* bundle and is rendered in the toolbar while parsing this topic. Later, if a different topic
* title is parsed from the html source, it gets updated.
*/
private final String topicTitle;
/**
* This topic's reply url
*/
private final String replyPageUrl;
private final ArrayList<Post> newPostsList;
/**
* The topicId of the loaded page
*/
private final int loadedPageTopicId;
/**
* Holds current page's index (starting from 1, not 0)
*/
private final int currentPageIndex;
/**
* Holds the requested topic's number of pages
*/
private final int pageCount;
/**
* The index of the post that has focus
*/
private final int focusedPostIndex;
//Topic's info related
private final String topicTreeAndMods;
private final String topicViewers;
/**
* The url of the last page that was attempted to be loaded
*/
private final String lastPageLoadAttemptedUrl;
private final SparseArray<String> pagesUrls;
public TopicTaskResult(TopicTask.ResultCode resultCode, String baseUrl, String topicTitle,
String replyPageUrl, ArrayList<Post> newPostsList, int loadedPageTopicId,
int currentPageIndex, int pageCount, int focusedPostIndex, String topicTreeAndMods,
String topicViewers, String lastPageLoadAttemptedUrl, SparseArray<String> pagesUrls) {
this.resultCode = resultCode;
this.baseUrl = baseUrl;
this.topicTitle = topicTitle;
this.replyPageUrl = replyPageUrl;
this.newPostsList = newPostsList;
this.loadedPageTopicId = loadedPageTopicId;
this.currentPageIndex = currentPageIndex;
this.pageCount = pageCount;
this.focusedPostIndex = focusedPostIndex;
this.topicTreeAndMods = topicTreeAndMods;
this.topicViewers = topicViewers;
this.lastPageLoadAttemptedUrl = lastPageLoadAttemptedUrl;
this.pagesUrls = pagesUrls;
}
public TopicTask.ResultCode getResultCode() {
return resultCode;
}
public String getBaseUrl() {
return baseUrl;
}
public String getTopicTitle() {
return topicTitle;
}
public String getReplyPageUrl() {
return replyPageUrl;
}
public ArrayList<Post> getNewPostsList() {
return newPostsList;
}
public int getLoadedPageTopicId() {
return loadedPageTopicId;
}
public int getCurrentPageIndex() {
return currentPageIndex;
}
public int getPageCount() {
return pageCount;
}
public int getFocusedPostIndex() {
return focusedPostIndex;
}
public String getTopicTreeAndMods() {
return topicTreeAndMods;
}
public String getTopicViewers() {
return topicViewers;
}
public String getLastPageLoadAttemptedUrl() {
return lastPageLoadAttemptedUrl;
}
public SparseArray<String> getPagesUrls() {
return pagesUrls;
}
}

6
app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java

@ -2,6 +2,7 @@ package gr.thmmy.mthmmy.base;
import android.Manifest; import android.Manifest;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -55,6 +56,7 @@ import gr.thmmy.mthmmy.model.ThmmyFile;
import gr.thmmy.mthmmy.services.DownloadHelper; import gr.thmmy.mthmmy.services.DownloadHelper;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.FileUtils; import gr.thmmy.mthmmy.utils.FileUtils;
import gr.thmmy.mthmmy.viewmodel.BaseViewModel;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import timber.log.Timber; import timber.log.Timber;
@ -107,6 +109,10 @@ public abstract class BaseActivity extends AppCompatActivity {
loadSavedBookmarks(); loadSavedBookmarks();
} }
BaseViewModel baseViewModel = ViewModelProviders.of(this).get(BaseViewModel.class);
baseViewModel.getCurrentPageBookmark().observe(this, thisPageBookmark -> {
setTopicBookmark(thisPageBookmarkMenuButton);
});
} }
@Override @Override

41
app/src/main/java/gr/thmmy/mthmmy/model/Post.java

@ -16,6 +16,10 @@ import java.util.Objects;
* previous fields</b>.</p> * previous fields</b>.</p>
*/ */
public class Post { public class Post {
public static final int TYPE_POST = 0;
public static final int TYPE_QUICK_REPLY = 1;
public static final int TYPE_EDIT = 2;
//Standard info (exists in every post) //Standard info (exists in every post)
private final String thumbnailUrl; private final String thumbnailUrl;
private final String author; private final String author;
@ -30,6 +34,8 @@ public class Post {
private final String lastEdit; private final String lastEdit;
private final String postURL; private final String postURL;
private final String postDeleteURL; private final String postDeleteURL;
private final String postEditURL;
private int postType;
//Extra info //Extra info
private final String profileURL; private final String profileURL;
@ -63,6 +69,8 @@ public class Post {
lastEdit = null; lastEdit = null;
postURL = null; postURL = null;
postDeleteURL = null; postDeleteURL = null;
postEditURL = null;
postType = -1;
} }
/** /**
@ -87,14 +95,14 @@ public class Post {
* @param userColor author's user color * @param userColor author's user color
* @param attachedFiles post's attached files * @param attachedFiles post's attached files
* @param lastEdit post's last edit date * @param lastEdit post's last edit date
* @param postURL post's URL * @param postURL post's URL
*/ */
public Post(@Nullable String thumbnailUrl, String author, String subject, String content public Post(@Nullable String thumbnailUrl, String author, String subject, String content
, int postIndex, int postNumber, String postDate, String profileURl, @Nullable String rank , int postIndex, int postNumber, String postDate, String profileURl, @Nullable String rank
, @Nullable String special_rank, @Nullable String gender, @Nullable String numberOfPosts , @Nullable String special_rank, @Nullable String gender, @Nullable String numberOfPosts
, @Nullable String personalText, int numberOfStars, int userColor , @Nullable String personalText, int numberOfStars, int userColor
, @Nullable ArrayList<ThmmyFile> attachedFiles, @Nullable String lastEdit, String postURL , @Nullable ArrayList<ThmmyFile> attachedFiles, @Nullable String lastEdit, String postURL
, @Nullable String postDeleteURL) { , @Nullable String postDeleteURL, @Nullable String postEditURL, int postType) {
if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null; if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null;
else this.thumbnailUrl = thumbnailUrl; else this.thumbnailUrl = thumbnailUrl;
this.author = author; this.author = author;
@ -116,6 +124,8 @@ public class Post {
this.numberOfStars = numberOfStars; this.numberOfStars = numberOfStars;
this.postURL = postURL; this.postURL = postURL;
this.postDeleteURL = postDeleteURL; this.postDeleteURL = postDeleteURL;
this.postEditURL = postEditURL;
this.postType = postType;
} }
/** /**
@ -138,7 +148,7 @@ public class Post {
public Post(@Nullable String thumbnailUrl, String author, String subject, String content public Post(@Nullable String thumbnailUrl, String author, String subject, String content
, int postIndex, int postNumber, String postDate, int userColor , int postIndex, int postNumber, String postDate, int userColor
, @Nullable ArrayList<ThmmyFile> attachedFiles, @Nullable String lastEdit, String postURL , @Nullable ArrayList<ThmmyFile> attachedFiles, @Nullable String lastEdit, String postURL
, @Nullable String postDeleteURL) { , @Nullable String postDeleteURL, @Nullable String postEditURL, int postType) {
if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null; if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null;
else this.thumbnailUrl = thumbnailUrl; else this.thumbnailUrl = thumbnailUrl;
this.author = author; this.author = author;
@ -160,6 +170,13 @@ public class Post {
numberOfStars = 0; numberOfStars = 0;
this.postURL = postURL; this.postURL = postURL;
this.postDeleteURL = postDeleteURL; this.postDeleteURL = postDeleteURL;
this.postEditURL = postEditURL;
this.postType = postType;
}
public static Post newQuickReply() {
return new Post(null, null, null, null, 0, 0, null,
0, null, null, null, null, null, TYPE_QUICK_REPLY);
} }
//Getters //Getters
@ -358,4 +375,22 @@ public class Post {
public String getPostDeleteURL() { public String getPostDeleteURL() {
return postDeleteURL; return postDeleteURL;
} }
/**
* Gets this post's modify url.
*
* @return post's edit url
*/
@Nullable
public String getPostEditURL() {
return postEditURL;
}
public int getPostType() {
return postType;
}
public void setPostType(int postType) {
this.postType = postType;
}
} }

76
app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java

@ -0,0 +1,76 @@
package gr.thmmy.mthmmy.utils;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.View;
import gr.thmmy.mthmmy.activities.board.BoardActivity;
import gr.thmmy.mthmmy.activities.profile.ProfileActivity;
import gr.thmmy.mthmmy.model.ThmmyPage;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_TITLE;
import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_URL;
import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_THUMBNAIL_URL;
import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_URL;
import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_USERNAME;
public class HTMLUtils {
private HTMLUtils() {}
public static SpannableStringBuilder getSpannableFromHtml(Activity activity, String html) {
CharSequence sequence;
if (Build.VERSION.SDK_INT >= 24) {
sequence = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
} else {
//noinspection deprecation
sequence = Html.fromHtml(html);
}
SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class);
for (URLSpan span : urls) {
makeLinkClickable(activity, strBuilder, span);
}
return strBuilder;
}
private static void makeLinkClickable(Activity activity, SpannableStringBuilder strBuilder, final URLSpan span) {
int start = strBuilder.getSpanStart(span);
int end = strBuilder.getSpanEnd(span);
int flags = strBuilder.getSpanFlags(span);
ClickableSpan clickable = new ClickableSpan() {
@Override
public void onClick(View view) {
ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(Uri.parse(span.getURL()));
if (target.is(ThmmyPage.PageCategory.BOARD)) {
Intent intent = new Intent(activity.getApplicationContext(), BoardActivity.class);
Bundle extras = new Bundle();
extras.putString(BUNDLE_BOARD_URL, span.getURL());
extras.putString(BUNDLE_BOARD_TITLE, "");
intent.putExtras(extras);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
activity.getApplicationContext().startActivity(intent);
} else if (target.is(ThmmyPage.PageCategory.PROFILE)) {
Intent intent = new Intent(activity.getApplicationContext(), ProfileActivity.class);
Bundle extras = new Bundle();
extras.putString(BUNDLE_PROFILE_URL, span.getURL());
extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, "");
extras.putString(BUNDLE_PROFILE_USERNAME, "");
intent.putExtras(extras);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
activity.getApplicationContext().startActivity(intent);
} else if (target.is(ThmmyPage.PageCategory.INDEX))
activity.finish();
}
};
strBuilder.setSpan(clickable, start, end, flags);
strBuilder.removeSpan(span);
}
}

18
app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java

@ -0,0 +1,18 @@
package gr.thmmy.mthmmy.viewmodel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import gr.thmmy.mthmmy.model.Bookmark;
public class BaseViewModel extends ViewModel {
protected MutableLiveData<Bookmark> currentPageBookmark;
public LiveData<Bookmark> getCurrentPageBookmark() {
if (currentPageBookmark == null) {
currentPageBookmark = new MutableLiveData<>();
}
return currentPageBookmark;
}
}

316
app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java

@ -0,0 +1,316 @@
package gr.thmmy.mthmmy.viewmodel;
import android.arch.lifecycle.MutableLiveData;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import java.util.ArrayList;
import gr.thmmy.mthmmy.activities.settings.SettingsActivity;
import gr.thmmy.mthmmy.activities.topic.tasks.DeleteTask;
import gr.thmmy.mthmmy.activities.topic.tasks.EditTask;
import gr.thmmy.mthmmy.activities.topic.tasks.PrepareForEditResult;
import gr.thmmy.mthmmy.activities.topic.tasks.PrepareForEditTask;
import gr.thmmy.mthmmy.activities.topic.tasks.PrepareForReply;
import gr.thmmy.mthmmy.activities.topic.tasks.PrepareForReplyResult;
import gr.thmmy.mthmmy.activities.topic.tasks.ReplyTask;
import gr.thmmy.mthmmy.activities.topic.tasks.TopicTask;
import gr.thmmy.mthmmy.activities.topic.tasks.TopicTaskResult;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.session.SessionManager;
public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTaskCompleted,
PrepareForReply.OnPrepareForReplyFinished, PrepareForEditTask.OnPrepareEditFinished {
/**
* topic state
*/
private boolean editingPost = false;
private boolean writingReply = false;
/**
* A list of {@link Post#getPostIndex()} for building quotes for replying
*/
private ArrayList<Integer> toQuoteList = new ArrayList<>();
/**
* caches the expand/collapse state of the user extra info in the current page for the recyclerview
*/
private ArrayList<Boolean> isUserExtraInfoVisibile = new ArrayList<>();
/**
* holds the adapter position of the post being edited
*/
private int postBeingEditedPosition;
private TopicTask currentTopicTask;
private PrepareForEditTask currentPrepareForEditTask;
private PrepareForReply currentPrepareForReplyTask;
//callbacks for topic activity
private TopicTask.TopicTaskObserver topicTaskObserver;
private DeleteTask.DeleteTaskCallbacks deleteTaskCallbacks;
private ReplyTask.ReplyTaskCallbacks replyFinishListener;
private PrepareForEditTask.PrepareForEditCallbacks prepareForEditCallbacks;
private EditTask.EditTaskCallbacks editTaskCallbacks;
private PrepareForReply.PrepareForReplyCallbacks prepareForReplyCallbacks;
private MutableLiveData<TopicTaskResult> topicTaskResult = new MutableLiveData<>();
private MutableLiveData<PrepareForReplyResult> prepareForReplyResult = new MutableLiveData<>();
private MutableLiveData<PrepareForEditResult> prepareForEditResult = new MutableLiveData<>();
private String firstTopicUrl;
public void initialLoad(String pageUrl) {
firstTopicUrl = pageUrl;
currentTopicTask = new TopicTask(topicTaskObserver, this);
currentTopicTask.execute(pageUrl);
}
public void loadUrl(String pageUrl) {
stopLoading();
currentTopicTask = new TopicTask(topicTaskObserver, this);
currentTopicTask.execute(pageUrl);
}
public void reloadPage() {
if (topicTaskResult.getValue() == null)
throw new NullPointerException("No topic task has finished yet!");
loadUrl(topicTaskResult.getValue().getLastPageLoadAttemptedUrl());
}
public void changePage(int pageRequested) {
if (topicTaskResult.getValue() == null)
throw new NullPointerException("No page has been loaded yet!");
if (pageRequested != topicTaskResult.getValue().getCurrentPageIndex() - 1)
loadUrl(topicTaskResult.getValue().getPagesUrls().get(pageRequested));
}
public void prepareForReply() {
if (topicTaskResult.getValue() == null)
throw new NullPointerException("Topic task has not finished yet!");
stopLoading();
changePage(topicTaskResult.getValue().getPageCount() - 1);
currentPrepareForReplyTask = new PrepareForReply(prepareForReplyCallbacks, this,
topicTaskResult.getValue().getReplyPageUrl());
currentPrepareForReplyTask.execute(toQuoteList.toArray(new Integer[0]));
}
public void postReply(Context context, String subject, String reply) {
if (prepareForReplyResult.getValue() == null) {
throw new NullPointerException("Reply preparation was not found!");
}
PrepareForReplyResult replyForm = prepareForReplyResult.getValue();
boolean includeAppSignature = true;
SessionManager sessionManager = BaseActivity.getSessionManager();
if (sessionManager.isLoggedIn()) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
includeAppSignature = prefs.getBoolean(SettingsActivity.POSTING_APP_SIGNATURE_ENABLE_KEY, true);
}
toQuoteList.clear();
new ReplyTask(replyFinishListener, includeAppSignature).execute(subject, reply,
replyForm.getNumReplies(), replyForm.getSeqnum(), replyForm.getSc(), replyForm.getTopic());
}
public void deletePost(String postDeleteUrl) {
new DeleteTask(deleteTaskCallbacks).execute(postDeleteUrl);
}
public void prepareForEdit(int position, String postEditURL) {
if (topicTaskResult.getValue() == null)
throw new NullPointerException("Topic task has not finished yet!");
stopLoading();
currentPrepareForEditTask = new PrepareForEditTask(prepareForEditCallbacks, this, position,
topicTaskResult.getValue().getReplyPageUrl());
currentPrepareForEditTask.execute(postEditURL);
}
public void editPost(int position, String subject, String message) {
if (prepareForEditResult.getValue() == null)
throw new NullPointerException("Edit preparation was not found!");
PrepareForEditResult editResult = prepareForEditResult.getValue();
new EditTask(editTaskCallbacks, position).execute(editResult.getCommitEditUrl(), message,
editResult.getNumReplies(), editResult.getSeqnum(), editResult.getSc(), subject, editResult.getTopic());
}
/**
* cancel tasks that change the ui
* topic, prepare for edit, prepare for reply tasks need to cancel all other ui changing tasks
* before starting
*/
public void stopLoading() {
if (currentTopicTask != null && currentTopicTask.getStatus() == AsyncTask.Status.RUNNING) {
currentTopicTask.cancel(true);
topicTaskObserver.onTopicTaskCancelled();
}
if (currentPrepareForEditTask != null && currentPrepareForEditTask.getStatus() == AsyncTask.Status.RUNNING) {
currentPrepareForEditTask.cancel(true);
prepareForEditCallbacks.onPrepareEditCancelled();
}
if (currentPrepareForReplyTask != null && currentPrepareForReplyTask.getStatus() == AsyncTask.Status.RUNNING) {
currentPrepareForReplyTask.cancel(true);
prepareForReplyCallbacks.onPrepareForReplyCancelled();
}
// no need to cancel reply, edit and delete task, user should not have to wait for the ui
// after he is done posting, editing or deleting
}
// callbacks for viewmodel
@Override
public void onTopicTaskCompleted(TopicTaskResult result) {
topicTaskResult.setValue(result);
if (result.getResultCode() == TopicTask.ResultCode.SUCCESS) {
isUserExtraInfoVisibile.clear();
for (int i = 0; i < result.getNewPostsList().size(); i++) {
isUserExtraInfoVisibile.add(false);
}
}
}
@Override
public void onPrepareForReplyFinished(PrepareForReplyResult result) {
writingReply = true;
prepareForReplyResult.setValue(result);
}
@Override
public void onPrepareEditFinished(PrepareForEditResult result, int position) {
editingPost = true;
postBeingEditedPosition = position;
prepareForEditResult.setValue(result);
}
// <-------------Just getters, setters and helper methods below here---------------->
public boolean isUserExtraInfoVisible(int position) {
return isUserExtraInfoVisibile.get(position);
}
public void hideUserInfo(int position) {
isUserExtraInfoVisibile.set(position, false);
}
public void toggleUserInfo(int position) {
isUserExtraInfoVisibile.set(position, !isUserExtraInfoVisibile.get(position));
}
public ArrayList<Integer> getToQuoteList() {
return toQuoteList;
}
public void postIndexToggle(Integer postIndex) {
if (toQuoteList.contains(postIndex))
toQuoteList.remove(postIndex);
else
toQuoteList.add(postIndex);
}
public void setTopicTaskObserver(TopicTask.TopicTaskObserver topicTaskObserver) {
this.topicTaskObserver = topicTaskObserver;
}
public void setDeleteTaskCallbacks(DeleteTask.DeleteTaskCallbacks deleteTaskCallbacks) {
this.deleteTaskCallbacks = deleteTaskCallbacks;
}
public void setReplyFinishListener(ReplyTask.ReplyTaskCallbacks replyFinishListener) {
this.replyFinishListener = replyFinishListener;
}
public void setPrepareForEditCallbacks(PrepareForEditTask.PrepareForEditCallbacks prepareForEditCallbacks) {
this.prepareForEditCallbacks = prepareForEditCallbacks;
}
public void setEditTaskCallbacks(EditTask.EditTaskCallbacks editTaskCallbacks) {
this.editTaskCallbacks = editTaskCallbacks;
}
public void setPrepareForReplyCallbacks(PrepareForReply.PrepareForReplyCallbacks prepareForReplyCallbacks) {
this.prepareForReplyCallbacks = prepareForReplyCallbacks;
}
public MutableLiveData<TopicTaskResult> getTopicTaskResult() {
return topicTaskResult;
}
public MutableLiveData<PrepareForReplyResult> getPrepareForReplyResult() {
return prepareForReplyResult;
}
public MutableLiveData<PrepareForEditResult> getPrepareForEditResult() {
return prepareForEditResult;
}
public void setEditingPost(boolean editingPost) {
this.editingPost = editingPost;
}
public boolean isEditingPost() {
return editingPost;
}
public int getPostBeingEditedPosition() {
return postBeingEditedPosition;
}
public boolean canReply() {
return topicTaskResult.getValue() != null && topicTaskResult.getValue().getReplyPageUrl() != null;
}
public boolean isWritingReply() {
return writingReply;
}
public void setWritingReply(boolean writingReply) {
this.writingReply = writingReply;
}
public String getBaseUrl() {
if (topicTaskResult.getValue() != null) {
return topicTaskResult.getValue().getBaseUrl();
} else {
return "";
}
}
public String getTopicUrl() {
if (topicTaskResult.getValue() != null) {
return topicTaskResult.getValue().getLastPageLoadAttemptedUrl();
} else {
// topic task has not finished yet (log? disable menu button until load is finished?)
return firstTopicUrl;
}
}
public String getTopicTitle() {
if (topicTaskResult.getValue() == null)
throw new NullPointerException("Topic task has not finished yet!");
return topicTaskResult.getValue().getTopicTitle();
}
public int getCurrentPageIndex() {
if (topicTaskResult.getValue() == null)
throw new NullPointerException("No page has been loaded yet!");
return topicTaskResult.getValue().getCurrentPageIndex();
}
public int getPageCount() {
if (topicTaskResult.getValue() == null)
throw new NullPointerException("No page has been loaded yet!");
return topicTaskResult.getValue().getPageCount();
}
public String getPostBeingEditedText() {
if (prepareForEditResult.getValue() == null)
throw new NullPointerException("Edit preparation was not found!");
return prepareForEditResult.getValue().getPostText();
}
public String getBuildedQuotes() {
if (prepareForReplyResult.getValue() != null) {
return prepareForReplyResult.getValue().getBuildedQuotes();
} else {
return "";
}
}
}

5
app/src/main/res/drawable/ic_edit_white_24dp.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

118
app/src/main/res/layout/activity_topic_edit_row.xml

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:paddingEnd="4dp"
android:paddingStart="4dp">
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardBackgroundColor="@color/card_background"
card_view:cardCornerRadius="5dp"
card_view:cardElevation="2dp"
card_view:cardPreventCornerOverlap="false"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/header"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp">
<FrameLayout
android:id="@+id/thumbnail_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
android:maxHeight="@dimen/thumbnail_size"
android:maxWidth="@dimen/thumbnail_size"
app:srcCompat="@drawable/ic_default_user_thumbnail_white_24dp" />
</FrameLayout>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/thumbnail_holder"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/post_author"
android:textColor="@color/primary_text"
android:textStyle="bold" />
<EditText
android:id="@+id/edit_message_subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/username"
android:layout_toEndOf="@+id/thumbnail_holder"
android:hint="@string/subject"
android:inputType="textMultiLine"
android:maxLength="80"
android:textSize="10sp"
tools:ignore="SmallSp" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<android.support.design.widget.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<EditText
android:id="@+id/edit_message_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/post_message"
android:inputType="textMultiLine" />
</android.support.design.widget.TextInputLayout>
<android.support.v7.widget.AppCompatImageButton
android:id="@+id/edit_message_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="5dp"
android:layout_marginEnd="5dp"
android:background="@color/card_background"
android:contentDescription="@string/submit"
app:srcCompat="@drawable/ic_send_accent_24dp" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</FrameLayout>

15
app/src/main/res/layout/activity_topic_overflow_menu.xml

@ -32,4 +32,19 @@
android:text="@string/post_delete_button" android:text="@string/post_delete_button"
android:textColor="@color/primary_text" /> android:textColor="@color/primary_text" />
<TextView
android:id="@+id/edit_post"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:background="?android:attr/selectableItemBackground"
android:drawablePadding="5dp"
android:drawableStart="@drawable/ic_edit_white_24dp"
android:gravity="center_vertical"
android:paddingBottom="6dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:text="@string/post_edit_button"
android:textColor="@color/primary_text"/>
</LinearLayout> </LinearLayout>

4
app/src/main/res/layout/activity_topic_quick_reply_row.xml

@ -74,7 +74,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/username" android:layout_below="@+id/username"
android:layout_toEndOf="@+id/thumbnail_holder" android:layout_toEndOf="@+id/thumbnail_holder"
android:hint="@string/quick_reply_subject" android:hint="@string/subject"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:maxLength="80" android:maxLength="80"
android:textSize="10sp" android:textSize="10sp"
@ -110,7 +110,7 @@
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:background="@color/card_background" android:background="@color/card_background"
android:contentDescription="@string/quick_reply_submit" android:contentDescription="@string/submit"
app:srcCompat="@drawable/ic_send_accent_24dp" /> app:srcCompat="@drawable/ic_send_accent_24dp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

6
app/src/main/res/values/strings.xml

@ -42,6 +42,7 @@
<string name="post_overflow_menu_button">Overflow menu button</string> <string name="post_overflow_menu_button">Overflow menu button</string>
<string name="post_share_button">Share</string> <string name="post_share_button">Share</string>
<string name="post_delete_button">Delete</string> <string name="post_delete_button">Delete</string>
<string name="post_edit_button">Edit</string>
<string name="user_number_of_posts">#%1$d</string> <string name="user_number_of_posts">#%1$d</string>
<string name="button_first">first</string> <string name="button_first">first</string>
<string name="button_previous">previous</string> <string name="button_previous">previous</string>
@ -49,8 +50,9 @@
<string name="button_next">next</string> <string name="button_next">next</string>
<string name="button_last">last</string> <string name="button_last">last</string>
<string name="quick_reply">Quick reply&#8230;</string> <string name="quick_reply">Quick reply&#8230;</string>
<string name="quick_reply_subject">Subject&#8230;</string> <string name="subject">Subject&#8230;</string>
<string name="quick_reply_submit">Submit</string> <string name="submit">Submit</string>
<string name="post_message">Message&#8230;</string>
<!--Profile Activity--> <!--Profile Activity-->
<string name="username">Username</string> <string name="username">Username</string>

Loading…
Cancel
Save