diff --git a/app/build.gradle b/app/build.gradle index cd627a53..dd59ec42 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,11 @@ android { archivesBaseName = archivesBaseName + "-$date" } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { @@ -37,7 +42,7 @@ dependencies { implementation 'com.android.support:cardview-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-messaging:17.0.0' + implementation 'com.google.firebase:firebase-messaging:17.1.0' implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.picasso:picasso:2.5.2' @@ -56,6 +61,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.0' implementation 'net.gotev:uploadservice: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' diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/Posting.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/Posting.java index cc08f17d..88c2f8a3 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/Posting.java +++ b/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. */ -class Posting { +public class Posting { /** * {@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 */ @@ -54,7 +54,7 @@ class Posting { * @return a {@link REPLY_STATUS} that describes the response status * @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() < 200 || response.code() >= 400) return REPLY_STATUS.OTHER_ERROR; String finalUrl = response.request().url().toString(); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java index 855f7045..3da39de4 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java @@ -2,27 +2,20 @@ package gr.thmmy.mthmmy.activities.topic; import android.annotation.SuppressLint; import android.app.NotificationManager; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.Rect; import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatDelegate; -import android.support.v7.preference.PreferenceManager; import android.support.v7.widget.RecyclerView; -import android.text.Html; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.text.style.URLSpan; -import android.util.SparseArray; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -35,40 +28,26 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Selector; - -import java.io.IOException; import java.util.ArrayList; import java.util.Objects; import gr.thmmy.mthmmy.R; -import gr.thmmy.mthmmy.activities.board.BoardActivity; -import gr.thmmy.mthmmy.activities.profile.ProfileActivity; -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.PrepareForEditTask; +import gr.thmmy.mthmmy.activities.topic.tasks.PrepareForReply; +import gr.thmmy.mthmmy.activities.topic.tasks.ReplyTask; +import gr.thmmy.mthmmy.activities.topic.tasks.TopicTask; import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.model.Bookmark; import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.utils.CustomLinearLayoutManager; -import gr.thmmy.mthmmy.utils.parsing.ParseException; -import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import gr.thmmy.mthmmy.utils.HTMLUtils; +import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; -import okhttp3.MultipartBody; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import timber.log.Timber; -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; -import static gr.thmmy.mthmmy.activities.topic.Posting.replyStatus; import static gr.thmmy.mthmmy.services.NotificationService.NEW_POST_TAG; /** @@ -78,7 +57,9 @@ import static gr.thmmy.mthmmy.services.NotificationService.NEW_POST_TAG; * key {@link #BUNDLE_TOPIC_TITLE} for faster title rendering. */ @SuppressWarnings("unchecked") -public class TopicActivity extends BaseActivity { +public class TopicActivity extends BaseActivity implements TopicTask.TopicTaskObserver, + DeleteTask.DeleteTaskCallbacks, ReplyTask.ReplyTaskCallbacks, PrepareForEditTask.PrepareForEditCallbacks, + EditTask.EditTaskCallbacks, PrepareForReply.PrepareForReplyCallbacks, TopicAdapter.OnPostFocusChangeListener { //Activity's variables /** * The key to use when putting topic's url String to {@link TopicActivity}'s Bundle. @@ -88,80 +69,18 @@ public class TopicActivity extends BaseActivity { * The key to use when putting topic's title String to {@link TopicActivity}'s Bundle. */ public static final String BUNDLE_TOPIC_TITLE = "TOPIC_TITLE"; - private static TopicTask topicTask; private MaterialProgressBar progressBar; private TextView toolbarTitle; - /** - * 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 static String base_url = ""; - /** - * 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, after topic's parsing - * is done, it gets the value of {@link #parsedTitle} if bundle title and parsed title differ. - */ - private String topicTitle; - /** - * Holds this topic's title as parsed from the html source. If this (parsed) title is different - * than the one that came with activity's bundle then the parsed title is preferred over the - * bundle one and gets rendered in the toolbar. - */ - private String parsedTitle; - private String topicPageUrl; private RecyclerView recyclerView; - /** - * Holds the url of this page - */ - private String loadedPageUrl = ""; - /** - * Holds the topicId of this page - */ - private int loadedPageTopicId = -1; - /** - * Becomes true after user has posted in this topic and the page is being reloaded and false - * when topic's reloading is done - */ - private boolean reloadingPage = false; //Posts related private TopicAdapter topicAdapter; /** * Holds a list of this topic's posts */ private ArrayList postsList; - /** - * Gets assigned to {@link #postFocus} when there is no post focus information in the url - */ - private static final int NO_POST_FOCUS = -1; - /** - * Holds the index of the post that has focus - */ - private int postFocus = NO_POST_FOCUS; - /** - * Holds the position in the {@link #postsList} of the post with focus - */ - private static int postFocusPosition = 0; //Reply related private FloatingActionButton replyFAB; - /** - * Holds this topic's reply url - */ - private String replyPageUrl = null; //Topic's pages related - /** - * Holds current page's index (starting from 1, not 0) - */ - private int thisPage = 1; - /** - * Holds this topic's number of pages - */ - private int numberOfPages = 1; - /** - * Holds a list of this topic's pages urls - */ - private final SparseArray pagesUrls = new SparseArray<>(); //Page select related /** * Used for handling bottom navigation bar's buttons long click user interactions @@ -196,12 +115,7 @@ public class TopicActivity extends BaseActivity { private TextView pageIndicator; private ImageButton nextPage; private ImageButton lastPage; - - //Topic's info related - private SpannableStringBuilder topicTreeAndMods = new SpannableStringBuilder("Loading..."), - topicViewers = new SpannableStringBuilder("Loading..."); - - boolean includeAppSignaturePreference = true; + private TopicViewModel viewModel; //Fix for vector drawables on android <21 static { @@ -215,9 +129,17 @@ public class TopicActivity extends BaseActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_topic); + viewModel = ViewModelProviders.of(this).get(TopicViewModel.class); + viewModel.setTopicTaskObserver(this); + viewModel.setDeleteTaskCallbacks(this); + viewModel.setReplyFinishListener(this); + viewModel.setPrepareForEditCallbacks(this); + viewModel.setEditTaskCallbacks(this); + viewModel.setPrepareForReplyCallbacks(this); + Bundle extras = getIntent().getExtras(); - topicTitle = extras.getString(BUNDLE_TOPIC_TITLE); - topicPageUrl = extras.getString(BUNDLE_TOPIC_URL); + String topicTitle = extras.getString(BUNDLE_TOPIC_TITLE); + String topicPageUrl = extras.getString(BUNDLE_TOPIC_URL); ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory( Uri.parse(topicPageUrl)); if (!target.is(ThmmyPage.PageCategory.TOPIC)) { @@ -227,12 +149,6 @@ public class TopicActivity extends BaseActivity { } topicPageUrl = ThmmyPage.sanitizeTopicUrl(topicPageUrl); - - if (sessionManager.isLoggedIn()) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - includeAppSignaturePreference = sharedPrefs.getBoolean(SettingsActivity.POSTING_APP_SIGNATURE_ENABLE_KEY, true); - } - thisPageBookmark = new Bookmark(topicTitle, ThmmyPage.getTopicId(topicPageUrl), true); //Initializes graphics @@ -257,35 +173,21 @@ public class TopicActivity extends BaseActivity { recyclerView = findViewById(R.id.topic_recycler_view); recyclerView.setHasFixedSize(true); - recyclerView.setOnTouchListener( - new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - v.performClick(); - return topicTask != null && topicTask.getStatus() == AsyncTask.Status.RUNNING; - } - } - ); //LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager( - getApplicationContext(), loadedPageUrl); + getApplicationContext(), topicPageUrl); recyclerView.setLayoutManager(layoutManager); - topicAdapter = new TopicAdapter(this, postsList, base_url, topicTask); + topicAdapter = new TopicAdapter(this, postsList); recyclerView.setAdapter(topicAdapter); replyFAB = findViewById(R.id.topic_fab); - replyFAB.setEnabled(false); + replyFAB.hide(); bottomNavBar = findViewById(R.id.bottom_navigation_bar); if (!sessionManager.isLoggedIn()) replyFAB.hide(); else { - replyFAB.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (sessionManager.isLoggedIn()) { - PrepareForReply prepareForReply = new PrepareForReply(); - prepareForReply.execute(topicAdapter.getToQuoteList()); - } - } + replyFAB.setOnClickListener(view -> { + if (sessionManager.isLoggedIn()) + viewModel.prepareForReply(); }); } @@ -300,11 +202,84 @@ public class TopicActivity extends BaseActivity { initDecrementButton(previousPage, SMALL_STEP); initIncrementButton(nextPage, SMALL_STEP); initIncrementButton(lastPage, LARGE_STEP); + paginationEnabled(false); - //Gets posts - topicTask = new TopicTask(); - topicTask.execute(topicPageUrl); //Attempt data parsing + viewModel.getTopicTaskResult().observe(this, topicTaskResult -> { + if (topicTaskResult == null) { + progressBar.setVisibility(ProgressBar.VISIBLE); + } else { + switch (topicTaskResult.getResultCode()) { + case SUCCESS: + if (topicTitle == null || Objects.equals(topicTitle, "") + || !Objects.equals(topicTitle, topicTaskResult.getTopicTitle())) { + toolbarTitle.setText(topicTaskResult.getTopicTitle()); + } + + recyclerView.getRecycledViewPool().clear(); //Avoid inconsistency detected bug + postsList.clear(); + postsList.addAll(topicTaskResult.getNewPostsList()); + topicAdapter.notifyDataSetChanged(); + + pageIndicator.setText(String.valueOf(topicTaskResult.getCurrentPageIndex()) + "/" + + String.valueOf(topicTaskResult.getPageCount())); + pageRequestValue = topicTaskResult.getCurrentPageIndex(); + paginationEnabled(true); + + if (topicTaskResult.getCurrentPageIndex() == topicTaskResult.getPageCount()) { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) + notificationManager.cancel(NEW_POST_TAG, topicTaskResult.getLoadedPageTopicId()); + } + + progressBar.setVisibility(ProgressBar.GONE); + if (topicTaskResult.getReplyPageUrl() == null) + replyFAB.hide(); + else + replyFAB.show(); + recyclerView.scrollToPosition(topicTaskResult.getFocusedPostIndex()); + break; + case NETWORK_ERROR: + Toast.makeText(getBaseContext(), "Network Error", Toast.LENGTH_SHORT).show(); + break; + case UNAUTHORIZED: + progressBar.setVisibility(ProgressBar.GONE); + Toast.makeText(getBaseContext(), "This topic is either missing or off limits to you", Toast.LENGTH_SHORT).show(); + break; + default: + //Parse failed - should never happen + Timber.d("Parse failed!"); //TODO report ParseException!!! + Toast.makeText(getBaseContext(), "Fatal Error", Toast.LENGTH_SHORT).show(); + finish(); + break; + } + } + + }); + viewModel.getPrepareForReplyResult().observe(this, prepareForReplyResult -> { + if (prepareForReplyResult != null) { + //prepare for a reply + postsList.add(Post.newQuickReply()); + topicAdapter.notifyItemInserted(postsList.size()); + recyclerView.scrollToPosition(postsList.size() - 1); + progressBar.setVisibility(ProgressBar.GONE); + replyFAB.hide(); + bottomNavBar.setVisibility(View.GONE); + } + + }); + viewModel.getPrepareForEditResult().observe(this, result -> { + if (result != null && result.isSuccessful()) { + viewModel.setEditingPost(true); + postsList.get(result.getPosition()).setPostType(Post.TYPE_EDIT); + topicAdapter.notifyItemChanged(result.getPosition()); + recyclerView.scrollToPosition(result.getPosition()); + progressBar.setVisibility(ProgressBar.GONE); + replyFAB.hide(); + bottomNavBar.setVisibility(View.GONE); + } + }); + viewModel.initialLoad(topicPageUrl); } @Override @@ -329,11 +304,22 @@ public class TopicActivity extends BaseActivity { LinearLayout infoDialog = (LinearLayout) inflater.inflate(R.layout.dialog_topic_info , null); TextView treeAndMods = infoDialog.findViewById(R.id.topic_tree_and_mods); - treeAndMods.setText(topicTreeAndMods); + treeAndMods.setText(new SpannableStringBuilder("Loading...")); treeAndMods.setMovementMethod(LinkMovementMethod.getInstance()); TextView usersViewing = infoDialog.findViewById(R.id.users_viewing); - usersViewing.setText(topicViewers); + usersViewing.setText(new SpannableStringBuilder("Loading...")); usersViewing.setMovementMethod(LinkMovementMethod.getInstance()); + viewModel.getTopicTaskResult().observe(this, topicTaskResult -> { + if (topicTaskResult == null) { + usersViewing.setText(new SpannableStringBuilder("Loading...")); + treeAndMods.setText(new SpannableStringBuilder("Loading...")); + } else { + String treeAndModsString = topicTaskResult.getTopicTreeAndMods(); + treeAndMods.setText(HTMLUtils.getSpannableFromHtml(this, treeAndModsString)); + String topicViewersString = topicTaskResult.getTopicViewers(); + usersViewing.setText(HTMLUtils.getSpannableFromHtml(this, topicViewersString)); + } + }); builder.setView(infoDialog); AlertDialog dialog = builder.create(); @@ -342,9 +328,9 @@ public class TopicActivity extends BaseActivity { case R.id.menu_share: Intent sendIntent = new Intent(android.content.Intent.ACTION_SEND); sendIntent.setType("text/plain"); - sendIntent.putExtra(android.content.Intent.EXTRA_TEXT, topicPageUrl); + sendIntent.putExtra(android.content.Intent.EXTRA_TEXT, viewModel.getTopicUrl()); startActivity(Intent.createChooser(sendIntent, "Share via")); - return true; + return true; //invalidateOptionsMenu(); default: return super.onOptionsItemSelected(item); } @@ -355,14 +341,21 @@ public class TopicActivity extends BaseActivity { if (drawer.isDrawerOpen()) { drawer.closeDrawer(); return; - } else if (postsList != null && postsList.size() > 0 && postsList.get(postsList.size() - 1) == null) { + } else if (viewModel.isWritingReply()) { postsList.remove(postsList.size() - 1); topicAdapter.notifyItemRemoved(postsList.size()); topicAdapter.setBackButtonHidden(); - replyFAB.setVisibility(View.INVISIBLE); - bottomNavBar.setVisibility(View.INVISIBLE); - paginationEnabled(true); - replyFAB.setEnabled(true); + viewModel.setWritingReply(false); + replyFAB.show(); + bottomNavBar.setVisibility(View.VISIBLE); + return; + } else if (viewModel.isEditingPost()) { + postsList.get(viewModel.getPostBeingEditedPosition()).setPostType(Post.TYPE_POST); + topicAdapter.notifyItemChanged(viewModel.getPostBeingEditedPosition()); + topicAdapter.setBackButtonHidden(); + viewModel.setEditingPost(false); + replyFAB.show(); + bottomNavBar.setVisibility(View.VISIBLE); return; } super.onBackPressed(); @@ -373,19 +366,18 @@ public class TopicActivity extends BaseActivity { super.onResume(); refreshTopicBookmark(); drawer.setSelection(-1); - - if (sessionManager.isLoggedIn()) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - includeAppSignaturePreference = sharedPrefs.getBoolean(SettingsActivity.POSTING_APP_SIGNATURE_ENABLE_KEY, true); - } } @Override protected void onDestroy() { super.onDestroy(); recyclerView.setAdapter(null); - if (topicTask != null && topicTask.getStatus() != AsyncTask.Status.RUNNING) - topicTask.cancel(true); + viewModel.stopLoading(); + } + + @Override + public void onPostFocusChange(int position) { + recyclerView.scrollToPosition(position); } //--------------------------------------BOTTOM NAV BAR METHODS---------------------------------- @@ -448,26 +440,23 @@ public class TopicActivity extends BaseActivity { @SuppressLint("ClickableViewAccessibility") private void initIncrementButton(ImageButton increment, final int step) { // Increment once for a click - increment.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - if (!autoIncrement && step == LARGE_STEP) { - changePage(numberOfPages - 1); - } else if (!autoIncrement) { - incrementPageRequestValue(step); - changePage(pageRequestValue - 1); - } + increment.setOnClickListener(v -> { + if (!autoIncrement && step == LARGE_STEP) { + incrementPageRequestValue(viewModel.getPageCount()); + viewModel.changePage(viewModel.getPageCount() - 1); + } else if (!autoIncrement) { + incrementPageRequestValue(step); + viewModel.changePage(pageRequestValue - 1); } }); // Auto increment for a long click increment.setOnLongClickListener( - new View.OnLongClickListener() { - public boolean onLongClick(View arg0) { - paginationDisable(arg0); - autoIncrement = true; - repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); - return false; - } + arg0 -> { + paginationDisable(arg0); + autoIncrement = true; + repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); + return false; } ); @@ -481,11 +470,11 @@ public class TopicActivity extends BaseActivity { } else if (rect != null && event.getAction() == MotionEvent.ACTION_UP && autoIncrement) { autoIncrement = false; paginationEnabled(true); - changePage(pageRequestValue - 1); + viewModel.changePage(pageRequestValue - 1); } else if (rect != null && event.getAction() == MotionEvent.ACTION_MOVE) { if (!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) { autoIncrement = false; - decrementPageRequestValue(pageRequestValue - thisPage); + decrementPageRequestValue(pageRequestValue - viewModel.getCurrentPageIndex()); paginationEnabled(true); } } @@ -497,26 +486,23 @@ public class TopicActivity extends BaseActivity { @SuppressLint("ClickableViewAccessibility") private void initDecrementButton(ImageButton decrement, final int step) { // Decrement once for a click - decrement.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - if (!autoDecrement && step == LARGE_STEP) { - changePage(0); - } else if (!autoDecrement) { - decrementPageRequestValue(step); - changePage(pageRequestValue - 1); - } + decrement.setOnClickListener(v -> { + if (!autoDecrement && step == LARGE_STEP) { + decrementPageRequestValue(viewModel.getPageCount()); + viewModel.changePage(0); + } else if (!autoDecrement) { + decrementPageRequestValue(step); + viewModel.changePage(pageRequestValue - 1); } }); // Auto decrement for a long click decrement.setOnLongClickListener( - new View.OnLongClickListener() { - public boolean onLongClick(View arg0) { - paginationDisable(arg0); - autoDecrement = true; - repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); - return false; - } + arg0 -> { + paginationDisable(arg0); + autoDecrement = true; + repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); + return false; } ); @@ -530,12 +516,12 @@ public class TopicActivity extends BaseActivity { } else if (event.getAction() == MotionEvent.ACTION_UP && autoDecrement) { autoDecrement = false; paginationEnabled(true); - changePage(pageRequestValue - 1); + viewModel.changePage(pageRequestValue - 1); } else if (event.getAction() == MotionEvent.ACTION_MOVE) { if (rect != null && !rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) { autoIncrement = false; - incrementPageRequestValue(thisPage - pageRequestValue); + incrementPageRequestValue(viewModel.getCurrentPageIndex() - pageRequestValue); paginationEnabled(true); } } @@ -545,11 +531,11 @@ public class TopicActivity extends BaseActivity { } private void incrementPageRequestValue(int step) { - if (pageRequestValue < numberOfPages - step) { + if (pageRequestValue < viewModel.getPageCount() - step) { pageRequestValue = pageRequestValue + step; } else - pageRequestValue = numberOfPages; - pageIndicator.setText(pageRequestValue + "/" + String.valueOf(numberOfPages)); + pageRequestValue = viewModel.getPageCount(); + pageIndicator.setText(pageRequestValue + "/" + String.valueOf(viewModel.getPageCount())); } private void decrementPageRequestValue(int step) { @@ -557,490 +543,113 @@ public class TopicActivity extends BaseActivity { pageRequestValue = pageRequestValue - step; else pageRequestValue = 1; - pageIndicator.setText(pageRequestValue + "/" + String.valueOf(numberOfPages)); + pageIndicator.setText(pageRequestValue + "/" + String.valueOf(viewModel.getPageCount())); } - private void changePage(int pageRequested) { - if (pageRequested != thisPage - 1) { - if (topicTask != null && topicTask.getStatus() != AsyncTask.Status.RUNNING) - topicTask.cancel(true); + //------------------------------------BOTTOM NAV BAR METHODS END------------------------------------ - topicTask = new TopicTask(); - topicTask.execute(pagesUrls.get(pageRequested)); //Attempt data parsing - } + @Override + public void onTopicTaskStarted() { + progressBar.setVisibility(ProgressBar.VISIBLE); } - //------------------------------------BOTTOM NAV BAR METHODS END------------------------------------ - private enum ResultCode { - SUCCESS, NETWORK_ERROR, PARSING_ERROR, OTHER_ERROR, SAME_PAGE, UNAUTHORIZED + @Override + public void onTopicTaskCancelled() { + progressBar.setVisibility(ProgressBar.GONE); } + @Override + public void onReplyTaskStarted() { + progressBar.setVisibility(ProgressBar.VISIBLE); + } - /** - * An {@link AsyncTask} that handles asynchronous fetching of this topic page and parsing of its - * data. - *

TopicTask's {@link AsyncTask#execute execute} method needs a topic's url as String - * parameter.

- */ - class TopicTask extends AsyncTask { - ArrayList localPostsList; - - @Override - protected void onPreExecute() { - progressBar.setVisibility(ProgressBar.VISIBLE); - paginationEnabled(false); - if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(false); - } - - protected ResultCode doInBackground(String... strings) { - Document document = null; - String newPageUrl = strings[0]; - - //Finds the index of message focus if present - { - postFocus = NO_POST_FOCUS; - 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("#"))); - } - } - //Checks if the page to be loaded is the one already shown - if (!reloadingPage && !Objects.equals(loadedPageUrl, "") && newPageUrl.contains(base_url)) { - if (newPageUrl.contains("topicseen#new") || newPageUrl.contains("#new")) - if (thisPage == numberOfPages) - return ResultCode.SAME_PAGE; - if (newPageUrl.contains("msg")) { - String tmpUrlSbstr = newPageUrl.substring(newPageUrl.indexOf("msg") + 3); - if (tmpUrlSbstr.contains("msg")) - tmpUrlSbstr = tmpUrlSbstr.substring(0, tmpUrlSbstr.indexOf("msg") - 1); - int testAgainst = Integer.parseInt(tmpUrlSbstr); - for (Post post : postsList) { - if (post.getPostIndex() == testAgainst) { - return ResultCode.SAME_PAGE; - } - } - } else if ((Objects.equals(newPageUrl, base_url) && thisPage == 1) || - Integer.parseInt(newPageUrl.substring(base_url.length() + 1)) / 15 + 1 == thisPage) - return ResultCode.SAME_PAGE; - } else if (!Objects.equals(loadedPageUrl, "")) topicTitle = null; - if (reloadingPage) reloadingPage = !reloadingPage; - - loadedPageUrl = newPageUrl; - if (strings[0].substring(0, strings[0].lastIndexOf(".")).contains("topic=")) - base_url = strings[0].substring(0, strings[0].lastIndexOf(".")); //New topic's base url - replyPageUrl = null; - Request request = new Request.Builder() - .url(newPageUrl) - .build(); - try { - Response response = client.newCall(request).execute(); - document = Jsoup.parse(response.body().string()); - localPostsList = parse(document); - - loadedPageTopicId = Integer.parseInt(ThmmyPage.getTopicId(loadedPageUrl)); - - //Finds the position of the focused message if present - for (int i = 0; i < localPostsList.size(); ++i) { - if (localPostsList.get(i).getPostIndex() == postFocus) { - postFocusPosition = i; - break; - } - } - return ResultCode.SUCCESS; - } catch (IOException e) { - Timber.i(e, "IO Exception"); - return ResultCode.NETWORK_ERROR; - } catch (ParseException e) { - if (isUnauthorized(document)) - return ResultCode.UNAUTHORIZED; - Timber.e(e, "Parsing Error"); - return ResultCode.PARSING_ERROR; - } catch (Exception e) { - Timber.e(e, "Exception"); - return ResultCode.OTHER_ERROR; - } - } - - protected void onPostExecute(ResultCode parseResult) { - switch (parseResult) { - case SUCCESS: - if (topicTitle == null || Objects.equals(topicTitle, "") - || !Objects.equals(topicTitle, parsedTitle)) { - toolbarTitle.setText(parsedTitle); - topicTitle = parsedTitle; - thisPageBookmark = new Bookmark(parsedTitle, Integer.toString(loadedPageTopicId), true); - invalidateOptionsMenu(); - } - - if (!postsList.isEmpty()) { - recyclerView.getRecycledViewPool().clear(); //Avoid inconsistency detected bug - postsList.clear(); - topicAdapter.notifyItemRangeRemoved(0, postsList.size() - 1); - } - postsList.addAll(localPostsList); - topicAdapter.notifyItemRangeInserted(0, postsList.size()); - topicAdapter.prepareForDelete(new DeleteTask()); - progressBar.setVisibility(ProgressBar.INVISIBLE); - - if (replyPageUrl == null) { - replyFAB.hide(); - topicAdapter.resetTopic(base_url, new TopicTask(), false); - } else topicAdapter.resetTopic(base_url, new TopicTask(), true); - - if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(true); - - //Set current page - pageIndicator.setText(String.valueOf(thisPage) + "/" + String.valueOf(numberOfPages)); - pageRequestValue = thisPage; - - if (thisPage == numberOfPages) { - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager != null) - notificationManager.cancel(NEW_POST_TAG, loadedPageTopicId); - } - - paginationEnabled(true); - break; - case NETWORK_ERROR: - Toast.makeText(getBaseContext(), "Network Error", Toast.LENGTH_SHORT).show(); - break; - case SAME_PAGE: - stopLoading(); - Toast.makeText(getBaseContext(), "That's the same page", Toast.LENGTH_SHORT).show(); - //TODO change focus - break; - case UNAUTHORIZED: - stopLoading(); - Toast.makeText(getBaseContext(), "This topic is either missing or off limits to you", Toast.LENGTH_SHORT).show(); - break; - default: - //Parse failed - should never happen - Timber.d("Parse failed!"); //TODO report ParseException!!! - Toast.makeText(getBaseContext(), "Fatal Error", Toast.LENGTH_SHORT).show(); - finish(); - break; - } - } - - private void stopLoading() { - progressBar.setVisibility(ProgressBar.INVISIBLE); - if (replyPageUrl == null) { - replyFAB.hide(); - topicAdapter.resetTopic(base_url, new TopicTask(), false); - } else topicAdapter.resetTopic(base_url, new TopicTask(), true); - if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(true); - paginationEnabled(true); - } - - /** - * All the parsing a topic needs. - * - * @param topic {@link Document} object containing this topic's source code - * @see org.jsoup.Jsoup Jsoup - */ - private ArrayList parse(Document topic) throws ParseException { - try { - ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic); - - //Finds topic's tree, mods and users viewing - { - topicTreeAndMods = getSpannableFromHtml(topic.select("div.nav").first().html()); - topicViewers = getSpannableFromHtml(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 - { - parsedTitle = topic.select("td[id=top_subject]").first().text(); - if (parsedTitle.contains("Topic:")) { - parsedTitle = parsedTitle.substring(parsedTitle.indexOf("Topic:") + 7 - , parsedTitle.indexOf("(Read") - 2); - } else { - parsedTitle = parsedTitle.substring(parsedTitle.indexOf("Θέμα:") + 6 - , parsedTitle.indexOf("(Αναγνώστηκε") - 2); - Timber.d("Parsed title: %s", parsedTitle); - } - } - - { //Finds current page's index - thisPage = TopicParser.parseCurrentPageIndex(topic, language); - } - { //Finds number of pages - numberOfPages = TopicParser.parseTopicNumberOfPages(topic, thisPage, language); - - for (int i = 0; i < numberOfPages; i++) { - //Generate each page's url from topic's base url +".15*numberOfPage" - pagesUrls.put(i, base_url + "." + String.valueOf(i * 15)); - } - } - - return TopicParser.parseTopic(topic, language); - } catch (Exception e) { - throw new ParseException("Parsing failed (TopicTask)"); - } + @Override + public void onReplyTaskFinished(boolean success) { + View view = getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } - 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; - } + postsList.remove(postsList.size() - 1); + topicAdapter.notifyItemRemoved(postsList.size()); - private void makeLinkClickable(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(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); - getApplicationContext().startActivity(intent); - } else if (target.is(ThmmyPage.PageCategory.PROFILE)) { - Intent intent = new Intent(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); - getApplicationContext().startActivity(intent); - } else if (target.is(ThmmyPage.PageCategory.INDEX)) - finish(); - } - }; - strBuilder.setSpan(clickable, start, end, flags); - strBuilder.removeSpan(span); - } + progressBar.setVisibility(ProgressBar.GONE); + replyFAB.show(); + bottomNavBar.setVisibility(View.VISIBLE); + viewModel.setWritingReply(false); - private SpannableStringBuilder getSpannableFromHtml(String html) { - CharSequence sequence; - if (Build.VERSION.SDK_INT >= 24) { - sequence = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); + if (success) { + if ((postsList.get(postsList.size() - 1).getPostNumber() + 1) % 15 == 0) { + viewModel.loadUrl(viewModel.getBaseUrl() + "." + 2147483647); } 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(strBuilder, span); + viewModel.reloadPage(); } - return strBuilder; + } else { + Toast.makeText(TopicActivity.this, "Post failed!", Toast.LENGTH_SHORT).show(); } } - class PrepareForReply extends AsyncTask, Void, Boolean> { - String numReplies, seqnum, sc, topic, buildedQuotes = ""; - - @Override - protected void onPreExecute() { - changePage(numberOfPages - 1); - progressBar.setVisibility(ProgressBar.VISIBLE); - paginationEnabled(false); - replyFAB.setEnabled(false); - replyFAB.hide(); - bottomNavBar.setVisibility(View.GONE); - } - - @Override - protected Boolean doInBackground(ArrayList... quoteList) { - Document document; - Request request = new Request.Builder() - .url(replyPageUrl + ";wap2") - .build(); - - 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."); - return false; - } - - for (Integer quotePosition : quoteList[0]) { - request = new Request.Builder() - .url("https://www.thmmy.gr/smf/index.php?action=quotefast;quote=" + - postsList.get(quotePosition).getPostIndex() + - ";" + "sesc=" + sc + ";xml") - .build(); - - try { - Response response = client.newCall(request).execute(); - String body = response.body().string(); - buildedQuotes += body.substring(body.indexOf("") + 7, body.indexOf("")); - buildedQuotes += "\n\n"; - } catch (IOException | Selector.SelectorParseException e) { - Timber.e(e, "Quote building failed."); - return false; - } - } - return true; - } + @Override + public void onPrepareForReplyStarted() { + progressBar.setVisibility(ProgressBar.VISIBLE); + } - @Override - protected void onPostExecute(Boolean result) { - postsList.add(null); - topicAdapter.notifyItemInserted(postsList.size()); - topicAdapter.prepareForReply(new ReplyTask(), topicTitle, numReplies, seqnum, sc, - topic, buildedQuotes); - recyclerView.scrollToPosition(postsList.size() - 1); - progressBar.setVisibility(ProgressBar.GONE); - } + @Override + public void onPrepareForReplyCancelled() { + progressBar.setVisibility(ProgressBar.GONE); } - class ReplyTask extends AsyncTask { + @Override + public void onDeleteTaskStarted() { + progressBar.setVisibility(ProgressBar.VISIBLE); + } - @Override - protected void onPreExecute() { - progressBar.setVisibility(ProgressBar.VISIBLE); - paginationEnabled(false); - replyFAB.setEnabled(false); - } + @Override + public void onDeleteTaskFinished(boolean result) { + progressBar.setVisibility(ProgressBar.GONE); - @Override - protected Boolean doInBackground(String... args) { - final String sentFrommTHMMY = includeAppSignaturePreference - ? "\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 { - 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; - } + if (result) { + viewModel.reloadPage(); + } else { + Toast.makeText(TopicActivity.this, "Post deleted!", Toast.LENGTH_SHORT).show(); } + } - @Override - protected void onPostExecute(Boolean result) { - View view = getCurrentFocus(); - if (view != null) { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - - postsList.remove(postsList.size() - 1); - topicAdapter.notifyItemRemoved(postsList.size()); - - progressBar.setVisibility(ProgressBar.GONE); - replyFAB.setVisibility(View.VISIBLE); - bottomNavBar.setVisibility(View.VISIBLE); - - if (!result) - Toast.makeText(TopicActivity.this, "Post failed!", Toast.LENGTH_SHORT).show(); - paginationEnabled(true); - replyFAB.setEnabled(true); - - if (result) { - topicTask = new TopicTask(); - if ((postsList.get(postsList.size() - 1).getPostNumber() + 1) % 15 == 0) - topicTask.execute(base_url + "." + 2147483647); - else { - reloadingPage = true; - topicTask.execute(loadedPageUrl); - } - } - } + @Override + public void onPrepareEditStarted() { + progressBar.setVisibility(ProgressBar.VISIBLE); } - class DeleteTask extends AsyncTask { + @Override + public void onPrepareEditCancelled() { + progressBar.setVisibility(ProgressBar.GONE); + } - @Override - protected void onPreExecute() { - progressBar.setVisibility(ProgressBar.VISIBLE); - paginationEnabled(false); - replyFAB.setEnabled(false); - } + @Override + public void onEditTaskStarted() { + progressBar.setVisibility(ProgressBar.VISIBLE); + } - @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 { - client.newCall(delete).execute(); - Response response = client.newCall(delete).execute(); - //Response response = client.newCall(delete).execute(); - switch (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 + public void onEditTaskFinished(boolean result, int position) { + View view = getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } - @Override - protected void onPostExecute(Boolean result) { - progressBar.setVisibility(ProgressBar.GONE); - replyFAB.setVisibility(View.VISIBLE); - bottomNavBar.setVisibility(View.VISIBLE); - - if (!result) - Toast.makeText(TopicActivity.this, "Post deleted!", Toast.LENGTH_SHORT).show(); - paginationEnabled(true); - replyFAB.setEnabled(true); + postsList.get(position).setPostType(Post.TYPE_POST); + topicAdapter.notifyItemChanged(position); + viewModel.setEditingPost(false); + progressBar.setVisibility(ProgressBar.GONE); + replyFAB.show(); + bottomNavBar.setVisibility(View.VISIBLE); - if (result) { - topicTask = new TopicTask(); - reloadingPage = true; - topicTask.execute(loadedPageUrl); - } + if (result) { + viewModel.reloadPage(); + } else { + Toast.makeText(TopicActivity.this, "Edit failed!", Toast.LENGTH_SHORT).show(); } } } \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java index dcf448be..c4d0e71b 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java +++ b/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.TargetApi; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; 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.widget.AppCompatImageButton; import android.support.v7.widget.RecyclerView; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -37,7 +35,6 @@ import android.widget.TextView; import com.squareup.picasso.Picasso; -import java.util.ArrayList; import java.util.List; 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.ThmmyPage; import gr.thmmy.mthmmy.utils.CircleTransform; +import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import timber.log.Timber; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -71,119 +69,72 @@ class TopicAdapter extends RecyclerView.Adapter { */ private static int THUMBNAIL_SIZE; private final Context context; - private String topicTitle; - private String baseUrl; - private final ArrayList toQuoteList = new ArrayList<>(); + private final OnPostFocusChangeListener postFocusListener; private final List postsList; - /** - * 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 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; + private TopicViewModel viewModel; /** * @param context the context of the {@link RecyclerView} * @param postsList List of {@link Post} objects to use */ - TopicAdapter(Context context, List postsList, String baseUrl, - TopicActivity.TopicTask topicTask) { + TopicAdapter(TopicActivity context, List postsList) { this.context = context; this.postsList = postsList; - this.baseUrl = baseUrl; - - 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 getToQuoteList() { - return toQuoteList; - } + this.postFocusListener = context; - void prepareForReply(TopicActivity.ReplyTask replyTask, String topicTitle, String numReplies, - 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; - } + viewModel = ViewModelProviders.of(context).get(TopicViewModel.class); - void prepareForDelete(TopicActivity.DeleteTask deleteTask) { - this.deleteTask = deleteTask; + THUMBNAIL_SIZE = (int) context.getResources().getDimension(R.dimen.thumbnail_size); } @Override public int getItemViewType(int position) { - return postsList.get(position) == null ? VIEW_TYPE_QUICK_REPLY : VIEW_TYPE_POST; + return postsList.get(position).getPostType(); } + @NonNull @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - if (viewType == VIEW_TYPE_POST) { + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == Post.TYPE_POST) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.activity_topic_post_row, parent, false); return new PostViewHolder(itemView); - } else if (viewType == VIEW_TYPE_QUICK_REPLY) { + } else if (viewType == Post.TYPE_QUICK_REPLY) { View view = LayoutInflater.from(parent.getContext()). inflate(R.layout.activity_topic_quick_reply_row, parent, false); view.findViewById(R.id.quick_reply_submit).setEnabled(true); final EditText quickReplyText = view.findViewById(R.id.quick_reply_text); quickReplyText.setFocusableInTouchMode(true); - quickReplyText.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - quickReplyText.post(new Runnable() { - @Override - public void run() { - InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(quickReplyText, InputMethodManager.SHOW_IMPLICIT); - } - }); - } - }); + quickReplyText.setOnFocusChangeListener((v, hasFocus) -> quickReplyText.post(() -> { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(quickReplyText, InputMethodManager.SHOW_IMPLICIT); + })); quickReplyText.requestFocus(); - //Default post subject - replyDataHolder[replySubject] = "Re: " + topicTitle; - //Build quotes - if (!Objects.equals(buildedQuotes, "")) - replyDataHolder[replyText] = buildedQuotes; - return new QuickReplyViewHolder(view, new CustomEditTextListener(replySubject), - new CustomEditTextListener(replyText)); + return new QuickReplyViewHolder(view); + } else if (viewType == Post.TYPE_EDIT) { + View view = LayoutInflater.from(parent.getContext()). + inflate(R.layout.activity_topic_edit_row, parent, false); + view.findViewById(R.id.edit_message_submit).setEnabled(true); + + 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"}) @Override - public void onBindViewHolder(final RecyclerView.ViewHolder currentHolder, + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder currentHolder, final int position) { if (currentHolder instanceof PostViewHolder) { final Post currentPost = postsList.get(position); @@ -245,12 +196,7 @@ class TopicAdapter extends RecyclerView.Adapter { attached.setTextColor(filesTextColor); attached.setPadding(0, 3, 0, 3); - attached.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - ((BaseActivity) context).downloadFile(attachedFile); - } - }); + attached.setOnClickListener(view -> ((BaseActivity) context).downloadFile(attachedFile)); holder.postFooter.addView(attached); } @@ -329,11 +275,11 @@ class TopicAdapter extends RecyclerView.Adapter { , "fonts/fontawesome-webfont.ttf")); String aStar = context.getResources().getString(R.string.fa_icon_star); - String usersStars = ""; + StringBuilder usersStars = new StringBuilder(); 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.setVisibility(View.VISIBLE); } else @@ -349,7 +295,7 @@ class TopicAdapter extends RecyclerView.Adapter { } else holder.cardChildLinear.setBackground(null); //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.setAlpha(1.0f); @@ -372,53 +318,39 @@ class TopicAdapter extends RecyclerView.Adapter { } if (!currentPost.isDeleted()) { //Sets graphics behavior - holder.thumbnail.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - //Clicking the thumbnail opens user's profile - Intent intent = new Intent(context, ProfileActivity.class); - Bundle extras = new Bundle(); - extras.putString(BUNDLE_PROFILE_URL, currentPost.getProfileURL()); - if (currentPost.getThumbnailURL() == null) - extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); - else - extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, currentPost.getThumbnailURL()); - extras.putString(BUNDLE_PROFILE_USERNAME, currentPost.getAuthor()); - intent.putExtras(extras); - intent.setFlags(FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } + holder.thumbnail.setOnClickListener(view -> { + //Clicking the thumbnail opens user's profile + Intent intent = new Intent(context, ProfileActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_PROFILE_URL, currentPost.getProfileURL()); + if (currentPost.getThumbnailURL() == null) + extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); + else + extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, currentPost.getThumbnailURL()); + extras.putString(BUNDLE_PROFILE_USERNAME, currentPost.getAuthor()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); }); - holder.header.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - //Clicking the header makes it expand/collapse - boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); - tmp[isUserExtraInfoVisibile] = !tmp[isUserExtraInfoVisibile]; - viewProperties.set(holder.getAdapterPosition(), tmp); - TopicAnimations.animateUserExtraInfoVisibility(holder.username, - holder.subject, Color.parseColor("#FFFFFF"), - Color.parseColor("#757575"), holder.userExtraInfo); - } + holder.header.setOnClickListener(v -> { + //Clicking the header makes it expand/collapse + viewModel.toggleUserInfo(holder.getAdapterPosition()); + 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 - holder.userExtraInfo.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); - tmp[isUserExtraInfoVisibile] = false; - viewProperties.set(holder.getAdapterPosition(), tmp); - - TopicAnimations.animateUserExtraInfoVisibility(holder.username, - holder.subject, Color.parseColor("#FFFFFF"), - Color.parseColor("#757575"), (LinearLayout) v); - } + holder.userExtraInfo.setOnClickListener(v -> { + viewModel.hideUserInfo(holder.getAdapterPosition()); + TopicAnimations.animateUserExtraInfoVisibility(holder.username, + holder.subject, Color.parseColor("#FFFFFF"), + Color.parseColor("#757575"), (LinearLayout) v); }); } else { holder.header.setOnClickListener(null); holder.userExtraInfo.setOnClickListener(null); } - + holder.overflowButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -449,8 +381,20 @@ class TopicAdapter extends RecyclerView.Adapter { 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("")) { deletePostButton.setVisibility(View.GONE); @@ -477,37 +421,25 @@ class TopicAdapter extends RecyclerView.Adapter { }); } - //Displays the popup - popUp.showAsDropDown(holder.overflowButton); - } + //Displays the popup + popUp.showAsDropDown(holder.overflowButton); }); //noinspection PointlessBooleanExpression,ConstantConditions - if (!BaseActivity.getSessionManager().isLoggedIn() || !canReply) { + if (!BaseActivity.getSessionManager().isLoggedIn() || !viewModel.canReply()) { holder.quoteToggle.setVisibility(View.GONE); } else { - if (viewProperties.get(position)[isQuoteButtonChecked]) + if (viewModel.getToQuoteList().contains(currentPost.getPostIndex())) holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked_accent_24dp); else holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked_grey_24dp); //Sets graphics behavior - holder.quoteToggle.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - boolean[] tmp = viewProperties.get(holder.getAdapterPosition()); - if (tmp[isQuoteButtonChecked]) { - if (toQuoteList.contains(postsList.indexOf(currentPost))) { - 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); - } + holder.quoteToggle.setOnClickListener(view -> { + viewModel.postIndexToggle(currentPost.getPostIndex()); + if (viewModel.getToQuoteList().contains(currentPost.getPostIndex())) + holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_checked_accent_24dp); + else + holder.quoteToggle.setImageResource(R.drawable.ic_format_quote_unchecked_grey_24dp); }); } } else if (currentHolder instanceof QuickReplyViewHolder) { @@ -525,41 +457,65 @@ class TopicAdapter extends RecyclerView.Adapter { .transform(new CircleTransform()) .into(holder.thumbnail); 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(replyDataHolder[replyText]); + holder.quickReply.setText(viewModel.getBuildedQuotes()); - holder.submitButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (holder.quickReplySubject.getText().toString().isEmpty()) return; - if (holder.quickReply.getText().toString().isEmpty()) return; - holder.submitButton.setEnabled(false); - replyTask.execute(holder.quickReplySubject.getText().toString(), - holder.quickReply.getText().toString(), numReplies, seqnum, sc, topic); - - holder.quickReplySubject.getText().clear(); - holder.quickReplySubject.setText("Re: " + topicTitle); - holder.quickReply.getText().clear(); - holder.submitButton.setEnabled(true); - } + + holder.submitButton.setOnClickListener(view -> { + if (holder.quickReplySubject.getText().toString().isEmpty()) return; + if (holder.quickReply.getText().toString().isEmpty()) return; + holder.submitButton.setEnabled(false); + + viewModel.postReply(context, holder.quickReplySubject.getText().toString(), + holder.quickReply.getText().toString()); + + holder.quickReplySubject.getText().clear(); + holder.quickReplySubject.setText("Re: " + viewModel.getTopicTitle()); + holder.quickReply.getText().clear(); + holder.submitButton.setEnabled(true); }); + + if (backPressHidden) { holder.quickReply.requestFocus(); 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) { - this.baseUrl = baseUrl; - this.topicTask = topicTask; - 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]); + if (backPressHidden) { + holder.editMessage.requestFocus(); + backPressHidden = false; + } } } @@ -629,19 +585,33 @@ class TopicAdapter extends RecyclerView.Adapter { final EditText quickReply, quickReplySubject; final AppCompatImageButton submitButton; - QuickReplyViewHolder(View quickReply, CustomEditTextListener replySubject - , CustomEditTextListener replyText) { + QuickReplyViewHolder(View quickReply) { super(quickReply); thumbnail = quickReply.findViewById(R.id.thumbnail); username = quickReply.findViewById(R.id.username); this.quickReply = quickReply.findViewById(R.id.quick_reply_text); - this.quickReply.addTextChangedListener(replyText); quickReplySubject = quickReply.findViewById(R.id.quick_reply_subject); - quickReplySubject.addTextChangedListener(replySubject); 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 * handle internally, it does. Otherwise user is prompt to open the link in a browser. @@ -667,26 +637,41 @@ class TopicAdapter extends RecyclerView.Adapter { final String uriString = uri.toString(); ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(uri); + viewModel.stopLoading(); if (target.is(ThmmyPage.PageCategory.TOPIC)) { //This url points to a topic - //Checks if this is the current topic - if (Objects.equals(uriString.substring(0, uriString.lastIndexOf(".")), baseUrl)) { - //Gets uri's targeted message's index number - String msgIndexReq = uriString.substring(uriString.indexOf("msg") + 3); - if (msgIndexReq.contains("#")) - msgIndexReq = msgIndexReq.substring(0, msgIndexReq.indexOf("#")); - else - msgIndexReq = msgIndexReq.substring(0, msgIndexReq.indexOf(";")); - - //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 + //Checks if the page to be loaded is the one already shown + if (uriString.contains(viewModel.getBaseUrl())) { + Timber.e("reached here!"); + if (uriString.contains("topicseen#new") || uriString.contains("#new")) { + if (viewModel.getCurrentPageIndex() == viewModel.getPageCount()) { + //same page + postFocusListener.onPostFocusChange(getItemCount() - 1); + Timber.e("new"); return true; } } - - topicTask.execute(uri.toString()); + if (uriString.contains("msg")) { + 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); @@ -727,25 +712,9 @@ class TopicAdapter extends RecyclerView.Adapter { } - private class CustomEditTextListener implements TextWatcher { - private final int positionInDataHolder; - - 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) { - } + //we need to set a callback to topic activity to scroll the recyclerview when post focus is requested + public interface OnPostFocusChangeListener { + void onPostFocusChange(int position); } /** diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java index 3ead462c..dd354bf7 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java @@ -28,7 +28,7 @@ import timber.log.Timber; *
  • {@link #parseTopicNumberOfPages(Document, int, ParseHelpers.Language)}
  • *
  • {@link #parseTopic(Document, ParseHelpers.Language)}
  • */ -class TopicParser { +public class TopicParser { //User colors private static final int USER_COLOR_BLACK = Color.parseColor("#000000"); 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 * @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)) return topic.select("td:containsOwn(διαβάζουν αυτό το θέμα)").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 * @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; if (language.is(ParseHelpers.Language.GREEK)) { @@ -102,7 +102,7 @@ class TopicParser { * @return int containing the number of pages * @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; if (language.is(ParseHelpers.Language.GREEK)) { @@ -140,7 +140,7 @@ class TopicParser { * @return {@link ArrayList} of {@link Post}s * @see org.jsoup.Jsoup Jsoup */ - static ArrayList parseTopic(Document topic, ParseHelpers.Language language) { + public static ArrayList parseTopic(Document topic, ParseHelpers.Language language) { //Method's variables final int NO_INDEX = -1; ArrayList parsedPostsList = new ArrayList<>(); @@ -157,7 +157,7 @@ class TopicParser { //Variables for Post constructor 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_postURL, p_deletePostURL; + p_postURL, p_deletePostURL, p_editPostURL; int p_postNum, p_postIndex, p_numberOfStars, p_userColor; boolean p_isDeleted = false; ArrayList p_attachedFiles; @@ -174,6 +174,7 @@ class TopicParser { p_attachedFiles = new ArrayList<>(); p_postLastEditDate = null; p_deletePostURL = null; + p_editPostURL = null; //Language independent parsing //Finds thumbnail url @@ -306,6 +307,12 @@ class TopicParser { 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 Element postDate = thisRow.select("div.smalltext:matches(on:)").first(); p_postDate = postDate.text(); @@ -431,13 +438,13 @@ class TopicParser { 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_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 //Add new post in postsList, only standard information needed parsedPostsList.add(new Post(p_thumbnailURL, p_userName, p_subject, p_post , 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; diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/DeleteTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/DeleteTask.java new file mode 100644 index 00000000..5d4a0531 --- /dev/null +++ b/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 { + 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); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/EditTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/EditTask.java new file mode 100644 index 00000000..f9fe5444 --- /dev/null +++ b/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 { + 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); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditResult.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditResult.java new file mode 100644 index 00000000..a8176072 --- /dev/null +++ b/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; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditTask.java new file mode 100644 index 00000000..7259d580 --- /dev/null +++ b/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 { + 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); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReply.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReply.java new file mode 100644 index 00000000..42ea2774 --- /dev/null +++ b/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 { + 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("") + 7, body.indexOf(""))); + 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); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReplyResult.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReplyResult.java new file mode 100644 index 00000000..b15f77a0 --- /dev/null +++ b/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; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/ReplyTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/ReplyTask.java new file mode 100644 index 00000000..69bd0035 --- /dev/null +++ b/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 { + 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); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java new file mode 100644 index 00000000..e233ae1c --- /dev/null +++ b/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. + *

    TopicTask's {@link AsyncTask#execute execute} method needs a topic's url as String + * parameter.

    + */ +public class TopicTask extends AsyncTask { + 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 newPostsList = null; + int loadedPageTopicId = -1; + int focusedPostIndex = 0; + SparseArray 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); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTaskResult.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTaskResult.java new file mode 100644 index 00000000..f9b76736 --- /dev/null +++ b/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 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 pagesUrls; + + public TopicTaskResult(TopicTask.ResultCode resultCode, String baseUrl, String topicTitle, + String replyPageUrl, ArrayList newPostsList, int loadedPageTopicId, + int currentPageIndex, int pageCount, int focusedPostIndex, String topicTreeAndMods, + String topicViewers, String lastPageLoadAttemptedUrl, SparseArray 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 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 getPagesUrls() { + return pagesUrls; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java b/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java index 32f3e891..94521fc9 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java @@ -2,6 +2,7 @@ package gr.thmmy.mthmmy.base; import android.Manifest; import android.app.ProgressDialog; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -55,6 +56,7 @@ import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.services.DownloadHelper; import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.utils.FileUtils; +import gr.thmmy.mthmmy.viewmodel.BaseViewModel; import okhttp3.OkHttpClient; import timber.log.Timber; @@ -107,6 +109,10 @@ public abstract class BaseActivity extends AppCompatActivity { loadSavedBookmarks(); } + BaseViewModel baseViewModel = ViewModelProviders.of(this).get(BaseViewModel.class); + baseViewModel.getCurrentPageBookmark().observe(this, thisPageBookmark -> { + setTopicBookmark(thisPageBookmarkMenuButton); + }); } @Override diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/Post.java b/app/src/main/java/gr/thmmy/mthmmy/model/Post.java index 3206061d..8641f288 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/model/Post.java +++ b/app/src/main/java/gr/thmmy/mthmmy/model/Post.java @@ -16,6 +16,10 @@ import java.util.Objects; * previous fields.

    */ 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) private final String thumbnailUrl; private final String author; @@ -30,6 +34,8 @@ public class Post { private final String lastEdit; private final String postURL; private final String postDeleteURL; + private final String postEditURL; + private int postType; //Extra info private final String profileURL; @@ -63,6 +69,8 @@ public class Post { lastEdit = null; postURL = null; postDeleteURL = null; + postEditURL = null; + postType = -1; } /** @@ -87,14 +95,14 @@ public class Post { * @param userColor author's user color * @param attachedFiles post's attached files * @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 , int postIndex, int postNumber, String postDate, String profileURl, @Nullable String rank , @Nullable String special_rank, @Nullable String gender, @Nullable String numberOfPosts , @Nullable String personalText, int numberOfStars, int userColor , @Nullable ArrayList attachedFiles, @Nullable String lastEdit, String postURL - , @Nullable String postDeleteURL) { + , @Nullable String postDeleteURL, @Nullable String postEditURL, int postType) { if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null; else this.thumbnailUrl = thumbnailUrl; this.author = author; @@ -116,6 +124,8 @@ public class Post { this.numberOfStars = numberOfStars; this.postURL = postURL; 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 , int postIndex, int postNumber, String postDate, int userColor , @Nullable ArrayList attachedFiles, @Nullable String lastEdit, String postURL - , @Nullable String postDeleteURL) { + , @Nullable String postDeleteURL, @Nullable String postEditURL, int postType) { if (Objects.equals(thumbnailUrl, "")) this.thumbnailUrl = null; else this.thumbnailUrl = thumbnailUrl; this.author = author; @@ -160,6 +170,13 @@ public class Post { numberOfStars = 0; this.postURL = postURL; 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 @@ -358,4 +375,22 @@ public class Post { public String getPostDeleteURL() { 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; + } } diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java b/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java new file mode 100644 index 00000000..2713a2a8 --- /dev/null +++ b/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); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java new file mode 100644 index 00000000..53ed84ef --- /dev/null +++ b/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 currentPageBookmark; + + public LiveData getCurrentPageBookmark() { + if (currentPageBookmark == null) { + currentPageBookmark = new MutableLiveData<>(); + } + return currentPageBookmark; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java new file mode 100644 index 00000000..1fb97407 --- /dev/null +++ b/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 toQuoteList = new ArrayList<>(); + /** + * caches the expand/collapse state of the user extra info in the current page for the recyclerview + */ + private ArrayList 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 = new MutableLiveData<>(); + private MutableLiveData prepareForReplyResult = new MutableLiveData<>(); + private MutableLiveData 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 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 getTopicTaskResult() { + return topicTaskResult; + } + + public MutableLiveData getPrepareForReplyResult() { + return prepareForReplyResult; + } + + public MutableLiveData 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 ""; + } + } +} diff --git a/app/src/main/res/drawable/ic_edit_white_24dp.xml b/app/src/main/res/drawable/ic_edit_white_24dp.xml new file mode 100644 index 00000000..46462b57 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_topic_edit_row.xml b/app/src/main/res/layout/activity_topic_edit_row.xml new file mode 100644 index 00000000..0e86a581 --- /dev/null +++ b/app/src/main/res/layout/activity_topic_edit_row.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_topic_overflow_menu.xml b/app/src/main/res/layout/activity_topic_overflow_menu.xml index 361dac72..95492862 100644 --- a/app/src/main/res/layout/activity_topic_overflow_menu.xml +++ b/app/src/main/res/layout/activity_topic_overflow_menu.xml @@ -32,4 +32,19 @@ android:text="@string/post_delete_button" android:textColor="@color/primary_text" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_topic_quick_reply_row.xml b/app/src/main/res/layout/activity_topic_quick_reply_row.xml index 7702994c..2554e788 100644 --- a/app/src/main/res/layout/activity_topic_quick_reply_row.xml +++ b/app/src/main/res/layout/activity_topic_quick_reply_row.xml @@ -74,7 +74,7 @@ android:layout_height="wrap_content" android:layout_below="@+id/username" android:layout_toEndOf="@+id/thumbnail_holder" - android:hint="@string/quick_reply_subject" + android:hint="@string/subject" android:inputType="textMultiLine" android:maxLength="80" android:textSize="10sp" @@ -110,7 +110,7 @@ android:layout_marginBottom="5dp" android:layout_marginEnd="5dp" android:background="@color/card_background" - android:contentDescription="@string/quick_reply_submit" + android:contentDescription="@string/submit" app:srcCompat="@drawable/ic_send_accent_24dp" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 309f24ce..879671a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,7 @@ Overflow menu button Share Delete + Edit #%1$d first previous @@ -49,8 +50,9 @@ next last Quick reply… - Subject… - Submit + Subject… + Submit + Message… Username