diff --git a/app/build.gradle b/app/build.gradle index 60fbf711..83e3c58e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,13 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + testOptions { + unitTests { + returnDefaultValues = true + includeAndroidResources = true + } + } } def firebaseReleaseProjectId = "mthmmy-release-3aef0" @@ -106,6 +113,7 @@ dependencies { implementation 'net.gotev:uploadservice:3.5.2' implementation 'net.gotev:uploadservice-okhttp:3.4.2' //TODO: Warning: v.3.5 depends on okhttp 3.13! implementation 'com.itkacher.okhttpprofiler:okhttpprofiler:1.0.5' //Plugin: https://plugins.jetbrains.com/plugin/11249-okhttp-profiler + testImplementation 'junit:junit:4.12' testImplementation 'org.powermock:powermock-core:2.0.2' testImplementation 'org.powermock:powermock-module-junit4:2.0.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fd302031..ca0b8150 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> + @@ -171,7 +172,7 @@ @@ -183,6 +184,16 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".activities.main.MainActivity" /> + + + + \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java index 846a8961..b754cfc3 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java @@ -24,7 +24,7 @@ import java.util.Objects; import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.activities.LoginActivity; -import gr.thmmy.mthmmy.activities.create_content.CreateContentActivity; +import gr.thmmy.mthmmy.activities.create_topic.CreateTopicActivity; import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.model.Board; import gr.thmmy.mthmmy.model.Bookmark; @@ -109,8 +109,8 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo newTopicFAB.setOnClickListener(view -> { if (sessionManager.isLoggedIn()) { if (newTopicUrl != null) { - Intent intent = new Intent(this, CreateContentActivity.class); - intent.putExtra(CreateContentActivity.EXTRA_NEW_TOPIC_URL, newTopicUrl); + Intent intent = new Intent(this, CreateTopicActivity.class); + intent.putExtra(CreateTopicActivity.EXTRA_NEW_TOPIC_URL, newTopicUrl); startActivity(intent); } } else { diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/CreatePMActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/CreatePMActivity.java new file mode 100644 index 00000000..90017dda --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/CreatePMActivity.java @@ -0,0 +1,126 @@ +package gr.thmmy.mthmmy.activities.create_pm; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.InputType; +import android.text.TextUtils; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.Toast; + +import com.google.android.material.textfield.TextInputLayout; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.profile.ProfileActivity; +import gr.thmmy.mthmmy.activities.settings.SettingsActivity; +import gr.thmmy.mthmmy.base.BaseActivity; +import gr.thmmy.mthmmy.editorview.EditorView; +import gr.thmmy.mthmmy.editorview.EmojiKeyboard; +import gr.thmmy.mthmmy.session.SessionManager; +import gr.thmmy.mthmmy.utils.ExternalAsyncTask; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import timber.log.Timber; + +public class CreatePMActivity extends BaseActivity implements ExternalAsyncTask.OnTaskStartedListener, ExternalAsyncTask.OnTaskFinishedListener { + + private MaterialProgressBar progressBar; + private EditorView contentEditor; + private TextInputLayout subjectInput; + private EmojiKeyboard emojiKeyboard; + private String username, sendPmUrl; + /** + * Used for example in quotes to pre-populate the EditorView with quoted text + */ + private String defaultContent; + + public static final String BUNDLE_SEND_PM_URL = "send-pm-url"; + public static final String BUNDLE_PM_CONTENT = "pm-content"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_create_pm); + + Intent callingIntent = getIntent(); + username = callingIntent.getStringExtra(ProfileActivity.BUNDLE_PROFILE_USERNAME); + sendPmUrl = callingIntent.getStringExtra(BUNDLE_SEND_PM_URL); + defaultContent = callingIntent.getStringExtra(BUNDLE_PM_CONTENT); + + //Initialize toolbar + toolbar = findViewById(R.id.toolbar); + toolbar.setTitle("Create topic"); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + progressBar = findViewById(R.id.progressBar); + + emojiKeyboard = findViewById(R.id.emoji_keyboard); + + subjectInput = findViewById(R.id.subject_input); + subjectInput.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT); + subjectInput.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE); + + contentEditor = findViewById(R.id.main_content_editorview); + contentEditor.setEmojiKeyboard(emojiKeyboard); + emojiKeyboard.registerEmojiInputField(contentEditor); + contentEditor.setOnSubmitListener(v -> { + if (TextUtils.isEmpty(subjectInput.getEditText().getText())) { + subjectInput.setError("Required"); + return; + } + if (TextUtils.isEmpty(contentEditor.getText())) { + contentEditor.setError("Required"); + return; + } + + boolean includeAppSignature = true; + SessionManager sessionManager = BaseActivity.getSessionManager(); + if (sessionManager.isLoggedIn()) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + includeAppSignature = prefs.getBoolean(SettingsActivity.POSTING_APP_SIGNATURE_ENABLE_KEY, true); + } + + SendPMTask sendPMTask = new SendPMTask(includeAppSignature); + sendPMTask.setOnTaskStartedListener(this); + sendPMTask.setOnTaskFinishedListener(this); + sendPMTask.execute(sendPmUrl, subjectInput.getEditText().getText().toString(), + contentEditor.getText().toString()); + }); + if (defaultContent != null) + contentEditor.setText(defaultContent); + } + + @Override + public void onBackPressed() { + if (emojiKeyboard.getVisibility() == View.VISIBLE) { + emojiKeyboard.setVisibility(View.GONE); + } else { + super.onBackPressed(); + } + } + + + @Override + public void onTaskStarted() { + Timber.i("New pm started being sent"); + progressBar.setVisibility(View.VISIBLE); + } + + @Override + public void onTaskFinished(Boolean success) { + progressBar.setVisibility(View.INVISIBLE); + if (success) { + Timber.i("New pm sent successfully"); + Toast.makeText(this, "Personal message sent successfully", Toast.LENGTH_SHORT).show(); + finish(); + } else { + Timber.w("Failed to send pm"); + Toast.makeText(getBaseContext(), "Failed to send PM. Check your connection", Toast.LENGTH_LONG).show(); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/SendPMTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/SendPMTask.java new file mode 100644 index 00000000..703e47b9 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/SendPMTask.java @@ -0,0 +1,88 @@ +package gr.thmmy.mthmmy.activities.create_pm; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +import java.io.IOException; + +import gr.thmmy.mthmmy.base.BaseApplication; +import gr.thmmy.mthmmy.utils.ExternalAsyncTask; +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 SendPMTask extends ExternalAsyncTask { + + private boolean includeAppSignature; + + public SendPMTask(boolean includeAppSignature) { + this.includeAppSignature = includeAppSignature; + } + + @Override + protected Boolean doInBackground(String... strings) { + Request request = new Request.Builder() + .url(strings[0] + ";wap2") + .build(); + + OkHttpClient client = BaseApplication.getInstance().getClient(); + + Document document; + String seqnum, sc, outbox, createTopicUrl, replied_to, folder, u; + try { + Response response = client.newCall(request).execute(); + document = Jsoup.parse(response.body().string()); + + seqnum = document.select("input[name=seqnum]").first().attr("value"); + sc = document.select("input[name=sc]").first().attr("value"); + outbox = document.select("input[name=outbox]").first().attr("value"); + replied_to = document.select("input[name=replied_to]").first().attr("value"); + folder = document.select("input[name=folder]").first().attr("value"); + u = document.select("input[name=u]").first().attr("value"); + createTopicUrl = document.select("form").first().attr("action"); + + final String appSignature = "\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", strings[2] + (includeAppSignature ? appSignature : "")) + .addFormDataPart("seqnum", seqnum) + .addFormDataPart("sc", sc) + .addFormDataPart("u", u) // recipient id + .addFormDataPart("subject", strings[1]) + .addFormDataPart("outbox", outbox) + .addFormDataPart("replied_to", replied_to) + .addFormDataPart("folder", folder) + .build(); + + Request pmRequest = new Request.Builder() + .url(createTopicUrl) + .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(pmRequest).execute(); + Response response2 = client.newCall(pmRequest).execute(); + switch (replyStatus(response2)) { + case SUCCESSFUL: + BaseApplication.getInstance().logFirebaseAnalyticsEvent("new_pm_sent", null); + return true; + default: + Timber.e("Malformed pmRequest. Request string: %s", pmRequest.toString()); + return false; + } + } catch (IOException e) { + return false; + } + } catch (IOException e) { + return false; + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/CreateTopicActivity.java similarity index 94% rename from app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java rename to app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/CreateTopicActivity.java index 920a03b9..ec661962 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/CreateTopicActivity.java @@ -1,4 +1,4 @@ -package gr.thmmy.mthmmy.activities.create_content; +package gr.thmmy.mthmmy.activities.create_topic; import android.content.Intent; import android.content.SharedPreferences; @@ -21,7 +21,7 @@ import gr.thmmy.mthmmy.session.SessionManager; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import timber.log.Timber; -public class CreateContentActivity extends BaseActivity implements NewTopicTask.NewTopicTaskCallbacks { +public class CreateTopicActivity extends BaseActivity implements NewTopicTask.NewTopicTaskCallbacks { public final static String EXTRA_NEW_TOPIC_URL = "new-topic-extra"; @@ -33,7 +33,7 @@ public class CreateContentActivity extends BaseActivity implements NewTopicTask. @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_create_content); + setContentView(R.layout.activity_create_topic); //Initialize toolbar toolbar = findViewById(R.id.toolbar); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/create_content/NewTopicTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/NewTopicTask.java similarity index 98% rename from app/src/main/java/gr/thmmy/mthmmy/activities/create_content/NewTopicTask.java rename to app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/NewTopicTask.java index 0749d367..b8ead6b7 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/create_content/NewTopicTask.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/NewTopicTask.java @@ -1,4 +1,4 @@ -package gr.thmmy.mthmmy.activities.create_content; +package gr.thmmy.mthmmy.activities.create_topic; import android.os.AsyncTask; diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxActivity.java new file mode 100644 index 00000000..25b09935 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxActivity.java @@ -0,0 +1,94 @@ +package gr.thmmy.mthmmy.activities.inbox; + +import android.os.Bundle; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.base.BaseActivity; +import gr.thmmy.mthmmy.pagination.BottomPaginationView; +import gr.thmmy.mthmmy.utils.NetworkResultCodes; +import gr.thmmy.mthmmy.viewmodel.InboxViewModel; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import timber.log.Timber; + +public class InboxActivity extends BaseActivity { + + private InboxViewModel inboxViewModel; + + private MaterialProgressBar progressBar; + private RecyclerView pmRecyclerview; + private InboxAdapter inboxAdapter; + private BottomPaginationView bottomPagination; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_inbox); + + //Initialize toolbar + toolbar = findViewById(R.id.toolbar); + toolbar.setTitle("Inbox"); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + createDrawer(); + drawer.setSelection(INBOX_ID); + + progressBar = findViewById(R.id.progress_bar); + pmRecyclerview = findViewById(R.id.inbox_recyclerview); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + pmRecyclerview.setLayoutManager(layoutManager); + inboxAdapter = new InboxAdapter(this); + pmRecyclerview.setAdapter(inboxAdapter); + bottomPagination = findViewById(R.id.bottom_pagination); + + inboxViewModel = new ViewModelProvider(this).get(InboxViewModel.class); + bottomPagination.setOnPageRequestedListener(inboxViewModel); + subscribeUI(); + + inboxViewModel.loadInbox(); + } + + private void subscribeUI() { + inboxViewModel.setOnInboxTaskStartedListener(() -> { + progressBar.setVisibility(View.VISIBLE); + Timber.d("inbox task started"); + }); + inboxViewModel.setOnInboxTaskFinishedListener((resultCode, inbox) -> { + progressBar.setVisibility(View.INVISIBLE); + if (resultCode == NetworkResultCodes.SUCCESSFUL) { + Timber.i("Successfully loaded inbox"); + inboxAdapter.notifyDataSetChanged(); + } else { + Timber.w("Failed to load inbox"); + Toast.makeText(this, "Failed to load inbox", Toast.LENGTH_SHORT).show(); + finish(); + } + }); + inboxViewModel.setOnInboxTaskCancelledListener(() -> { + progressBar.setVisibility(ProgressBar.GONE); + Timber.d("inbox task cancelled"); + }); + inboxViewModel.getPageIndicatorIndex().observe(this, pageIndicatorIndex -> { + if (pageIndicatorIndex == null) return; + bottomPagination.setIndicatedPageIndex(pageIndicatorIndex); + }); + inboxViewModel.getPageCount().observe(this, pageCount -> { + if (pageCount == null) return; + bottomPagination.setTotalPageCount(pageCount); + }); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxAdapter.java new file mode 100644 index 00000000..504d78c7 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxAdapter.java @@ -0,0 +1,374 @@ +package gr.thmmy.mthmmy.activities.inbox; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupWindow; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.res.ResourcesCompat; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.RecyclerView; + +import com.squareup.picasso.Picasso; + +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.create_pm.CreatePMActivity; +import gr.thmmy.mthmmy.activities.profile.ProfileActivity; +import gr.thmmy.mthmmy.activities.topic.TopicActivity; +import gr.thmmy.mthmmy.model.PM; +import gr.thmmy.mthmmy.model.ThmmyPage; +import gr.thmmy.mthmmy.utils.CircleTransform; +import gr.thmmy.mthmmy.utils.MessageAnimations; +import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import gr.thmmy.mthmmy.viewmodel.InboxViewModel; + +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.TopicActivity.BUNDLE_TOPIC_URL; +import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.USER_COLOR_WHITE; +import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.USER_COLOR_YELLOW; + +public class InboxAdapter extends RecyclerView.Adapter { + + private Context context; + private InboxViewModel inboxViewModel; + + public InboxAdapter(InboxActivity context) { + this.context = context; + inboxViewModel = ViewModelProviders.of(context).get(InboxViewModel.class); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.activity_inbox_pm_row, parent, false); + return new InboxAdapter.PMViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + InboxAdapter.PMViewHolder holder = (PMViewHolder) viewHolder; + PM currentPM = pms().get(position); + + //Post's WebView parameters + holder.pm.setClickable(true); + holder.pm.setWebViewClient(new LinkLauncher()); + + Picasso.with(context) + .load(currentPM.getThumbnailUrl()) + .fit() + .centerCrop() + .error(Objects.requireNonNull(ResourcesCompat.getDrawable(context.getResources() + , R.drawable.ic_default_user_avatar_darker, null))) + .placeholder(Objects.requireNonNull(ResourcesCompat.getDrawable(context.getResources() + , R.drawable.ic_default_user_avatar_darker, null))) + .transform(new CircleTransform()) + .into(holder.thumbnail); + + //Sets username,submit date, index number, subject, post's and attached files texts + holder.username.setText(currentPM.getAuthor()); + holder.pmDate.setText(currentPM.getPmDate()); + holder.subject.setText(currentPM.getSubject()); + holder.pm.loadDataWithBaseURL("file:///android_asset/", currentPM.getContent(), + "text/html", "UTF-8", null); + + // author info + if (currentPM.getAuthorSpecialRank() != null && !currentPM.getAuthorSpecialRank().equals("")) { + holder.specialRank.setText(currentPM.getAuthorSpecialRank()); + holder.specialRank.setVisibility(View.VISIBLE); + } else holder.specialRank.setVisibility(View.GONE); + if (currentPM.getAuthorRank() != null && !currentPM.getAuthorRank().equals("")) { + holder.rank.setText(currentPM.getAuthorRank()); + holder.rank.setVisibility(View.VISIBLE); + } else holder.rank.setVisibility(View.GONE); + if (currentPM.getAuthorGender() != null && !currentPM.getAuthorGender().equals("")) { + holder.gender.setText(currentPM.getAuthorGender()); + holder.gender.setVisibility(View.VISIBLE); + } else holder.gender.setVisibility(View.GONE); + if (currentPM.getAuthorNumberOfPosts() != null && !currentPM.getAuthorNumberOfPosts().equals("")) { + holder.numberOfPosts.setText(currentPM.getAuthorNumberOfPosts()); + holder.numberOfPosts.setVisibility(View.VISIBLE); + } else holder.numberOfPosts.setVisibility(View.GONE); + if (currentPM.getAuthorPersonalText() != null && !currentPM.getAuthorPersonalText().equals("")) { + holder.personalText.setText(currentPM.getAuthorPersonalText()); + holder.personalText.setVisibility(View.VISIBLE); + } else holder.personalText.setVisibility(View.GONE); + if (currentPM.getAuthorColor() != USER_COLOR_YELLOW) + holder.username.setTextColor(currentPM.getAuthorColor()); + else holder.username.setTextColor(USER_COLOR_WHITE); + + if (currentPM.getAuthorNumberOfStars() > 0) { + holder.stars.setTypeface(Typeface.createFromAsset(context.getAssets() + , "fonts/fontawesome-webfont.ttf")); + + String aStar = context.getResources().getString(R.string.fa_icon_star); + StringBuilder usersStars = new StringBuilder(); + for (int i = 0; i < currentPM.getAuthorNumberOfStars(); ++i) { + usersStars.append(aStar); + } + holder.stars.setText(usersStars.toString()); + holder.stars.setTextColor(currentPM.getAuthorColor()); + holder.stars.setVisibility(View.VISIBLE); + } else holder.stars.setVisibility(View.GONE); + + if (currentPM.isUserMentioned()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + holder.cardChildLinear.setBackground(context.getResources(). + getDrawable(R.drawable.mention_card, null)); + } else + holder.cardChildLinear.setBackground(context.getResources(). + getDrawable(R.drawable.mention_card)); + } else if (currentPM.getAuthorColor() == ParseHelpers.USER_COLOR_PINK) { + //Special card for special member of the month! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + holder.cardChildLinear.setBackground(context.getResources(). + getDrawable(R.drawable.member_of_the_month_card, null)); + } else + holder.cardChildLinear.setBackground(context.getResources(). + getDrawable(R.drawable.member_of_the_month_card)); + } else holder.cardChildLinear.setBackground(null); + + //Avoid's view's visibility recycling + if (inboxViewModel.isUserExtraInfoVisible(holder.getAdapterPosition())) { + holder.userExtraInfo.setVisibility(View.VISIBLE); + holder.userExtraInfo.setAlpha(1.0f); + + holder.username.setMaxLines(Integer.MAX_VALUE); + holder.username.setEllipsize(null); + + holder.subject.setTextColor(Color.parseColor("#FFFFFF")); + holder.subject.setMaxLines(Integer.MAX_VALUE); + holder.subject.setEllipsize(null); + } else { + holder.userExtraInfo.setVisibility(View.GONE); + holder.userExtraInfo.setAlpha(0.0f); + + holder.username.setMaxLines(1); + holder.username.setEllipsize(TextUtils.TruncateAt.END); + + holder.subject.setTextColor(Color.parseColor("#757575")); + holder.subject.setMaxLines(1); + holder.subject.setEllipsize(TextUtils.TruncateAt.END); + } + + //Sets graphics behavior + 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, currentPM.getAuthorProfileUrl()); + if (currentPM.getThumbnailUrl() == null) + extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); + else + extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, currentPM.getAuthorProfileUrl()); + extras.putString(BUNDLE_PROFILE_USERNAME, currentPM.getAuthor()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + }); + holder.header.setOnClickListener(v -> { + //Clicking the header makes it expand/collapse + inboxViewModel.toggleUserInfo(holder.getAdapterPosition()); + MessageAnimations.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(v -> { + inboxViewModel.hideUserInfo(holder.getAdapterPosition()); + MessageAnimations.animateUserExtraInfoVisibility(holder.username, + holder.subject, Color.parseColor("#FFFFFF"), + Color.parseColor("#757575"), (LinearLayout) v); + }); + + holder.overflowButton.setOnClickListener(view -> { + LayoutInflater layoutInflater = LayoutInflater.from(context); + View popupContent = layoutInflater.inflate(R.layout.activity_inbox_overflow_menu, null); + + //Creates the PopupWindow + final PopupWindow popUp = new PopupWindow(holder.overflowButton.getContext()); + popUp.setContentView(popupContent); + popUp.setWidth(LinearLayout.LayoutParams.WRAP_CONTENT); + popUp.setHeight(LinearLayout.LayoutParams.WRAP_CONTENT); + popUp.setFocusable(true); + + TextView quoteButton = popupContent.findViewById(R.id.pm_quote_button); + quoteButton.setVisibility(View.GONE); // TODO + Drawable quoteDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_format_quote); + quoteButton.setCompoundDrawablesRelativeWithIntrinsicBounds(quoteDrawable, null, null, null); + quoteButton.setOnClickListener(v -> { + Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show(); + // TODO: Create delete PM task + }); + + final TextView replyButton = popupContent.findViewById(R.id.pm_reply_button); + Drawable replyDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_reply); + replyButton.setCompoundDrawablesRelativeWithIntrinsicBounds(replyDrawable, null, null, null); + + replyButton.setOnClickListener(v -> { + Intent sendPMIntent = new Intent(context, CreatePMActivity.class); + sendPMIntent.putExtra(CreatePMActivity.BUNDLE_SEND_PM_URL, currentPM.getReplyUrl()); + context.startActivity(sendPMIntent); + popUp.dismiss(); + }); + + TextView deletePostButton = popupContent.findViewById(R.id.delete_post); + + Drawable deleteStartDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_delete_white_24dp); + deletePostButton.setVisibility(View.GONE); //TODO + deletePostButton.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteStartDrawable, null, null, null); + popupContent.findViewById(R.id.delete_post).setOnClickListener(v -> { + new AlertDialog.Builder(holder.overflowButton.getContext()) + .setTitle("Delete personal message") + .setMessage("Do you really want to delete this personal message?") + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { + Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show(); + // TODO: Create delete PM task + }) + .setNegativeButton(android.R.string.no, null).show(); + popUp.dismiss(); + }); + + //Displays the popup + popUp.showAsDropDown(holder.overflowButton); + }); + } + + @Override + public int getItemCount() { + return inboxViewModel.getInbox() == null ? 0 : pms().size(); + } + + private ArrayList pms() { + return inboxViewModel.getInbox().getPms(); + } + + static class PMViewHolder extends RecyclerView.ViewHolder { + final LinearLayout cardChildLinear; + final TextView pmDate, username, subject; + final ImageView thumbnail; + final public WebView pm; + final ImageButton overflowButton; + final RelativeLayout header; + final LinearLayout userExtraInfo; + + final TextView specialRank, rank, gender, numberOfPosts, personalText, stars; + + PMViewHolder(@NonNull View view) { + super(view); + cardChildLinear = view.findViewById(R.id.card_child_linear); + pmDate = view.findViewById(R.id.pm_date); + thumbnail = view.findViewById(R.id.thumbnail); + username = view.findViewById(R.id.username); + subject = view.findViewById(R.id.subject); + pm = view.findViewById(R.id.pm); + pm.setBackgroundColor(Color.argb(1, 255, 255, 255)); + overflowButton = view.findViewById(R.id.pm_overflow_menu); + + //User's extra info + header = view.findViewById(R.id.header); + userExtraInfo = view.findViewById(R.id.user_extra_info); + specialRank = view.findViewById(R.id.special_rank); + rank = view.findViewById(R.id.rank); + gender = view.findViewById(R.id.gender); + numberOfPosts = view.findViewById(R.id.number_of_posts); + personalText = view.findViewById(R.id.personal_text); + stars = view.findViewById(R.id.stars); + } + } + + /** + * 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. + */ + private class LinkLauncher extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + final Uri uri = Uri.parse(url); + return handleUri(uri); + } + + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + final Uri uri = request.getUrl(); + return handleUri(uri); + } + + @SuppressWarnings("SameReturnValue") + private boolean handleUri(final Uri uri) { + final String uriString = uri.toString(); + + ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(uri); + if (target.is(ThmmyPage.PageCategory.TOPIC)) { + //This url points to a topic + Intent intent = new Intent(context, TopicActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_TOPIC_URL, uriString); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } else if (target.is(ThmmyPage.PageCategory.BOARD)) { + Intent intent = new Intent(context, BoardActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_BOARD_URL, uriString); + extras.putString(BUNDLE_BOARD_TITLE, ""); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } else if (target.is(ThmmyPage.PageCategory.PROFILE)) { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_PROFILE_URL, uriString); + extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); + extras.putString(BUNDLE_PROFILE_USERNAME, ""); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } + + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + //Method always returns true as no url should be loaded in the WebViews + return true; + } + + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/tasks/InboxTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/tasks/InboxTask.java new file mode 100644 index 00000000..aef6623e --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/inbox/tasks/InboxTask.java @@ -0,0 +1,203 @@ +package gr.thmmy.mthmmy.activities.inbox.tasks; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import gr.thmmy.mthmmy.base.BaseActivity; +import gr.thmmy.mthmmy.model.Inbox; +import gr.thmmy.mthmmy.model.PM; +import gr.thmmy.mthmmy.utils.NetworkResultCodes; +import gr.thmmy.mthmmy.utils.parsing.NewParseTask; +import gr.thmmy.mthmmy.utils.parsing.ParseException; +import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import okhttp3.Response; + +public class InboxTask extends NewParseTask { + @Override + protected Inbox parse(Document document, Response response) throws ParseException { + Inbox inbox = new Inbox(); + ParseHelpers.deobfuscateElements(document.select("span.__cf_email__,a.__cf_email__"), true); + + ParseHelpers.Language language = ParseHelpers.Language.getLanguage(document); + + inbox.setCurrentPageIndex(ParseHelpers.parseCurrentPageIndexInbox(document, language)); + inbox.setNumberOfPages(ParseHelpers.parseNumberOfPagesInbox(document, inbox.getCurrentPageIndex(), language)); + + ArrayList pmList = parsePMs(document, language); + inbox.setPms(pmList); + return inbox; + } + + @Override + protected int getResultCode(Response response, Inbox data) { + return NetworkResultCodes.SUCCESSFUL; + } + + private ArrayList parsePMs(Document document, ParseHelpers.Language language) { + ArrayList pms = new ArrayList<>(); + Elements pmContainerContainers = document.select("td[style=padding: 1px 1px 0 1px;]"); + for (Element pmContainerContainer : pmContainerContainers) { + PM pm = new PM(); + boolean isAuthorDeleted; + Element pmContainer = pmContainerContainer.select("table[style=table-layout: fixed;]").first().child(0); + + Element thumbnail = pmContainer.select("img.avatar").first(); + // User might not have an avatar + if (thumbnail != null) + pm.setThumbnailUrl(thumbnail.attr("src")); + + Element subjectAndDateContainer = pmContainer.select("td[align=left]").first(); + pm.setSubject(subjectAndDateContainer.select("b").first().text()); + Element dateContainer = subjectAndDateContainer.select("div").first(); + pm.setPmDate(subjectAndDateContainer.select("div").first().text()); + + String content = ParseHelpers.youtubeEmbeddedFix(pmContainer.select("div.personalmessage").first()); + //Adds stuff to make it work in WebView + //style.css + content = "" + content; + pm.setContent(content); + + pm.setQuoteUrl(pmContainer.select("img[src=https://www.thmmy.gr/smf/Themes/scribbles2_114/images/buttons/quote.gif]") + .first().parent().attr("href")); + pm.setReplyUrl(pmContainer.select("img[src=https://www.thmmy.gr/smf/Themes/scribbles2_114/images/buttons/im_reply.gif]") + .first().parent().attr("href")); + pm.setDeleteUrl(pmContainer.select("img[src=https://www.thmmy.gr/smf/Themes/scribbles2_114/images/buttons/delete.gif]") + .first().parent().attr("href")); + + // language specific parsing + Element username; + if (language == ParseHelpers.Language.GREEK) { + //Finds username and profile's url + username = pmContainer.select("a[title^=Εμφάνιση προφίλ του μέλους]").first(); + if (username == null) { //Deleted profile + isAuthorDeleted = true; + String authorName = pmContainer.select("td:has(div.smalltext:containsOwn(Επισκέπτης))[style^=overflow]") + .first().text(); + authorName = authorName.substring(0, authorName.indexOf(" Επισκέπτης")); + pm.setAuthor(authorName); + pm.setAuthorColor(ParseHelpers.USER_COLOR_YELLOW); + } else { + isAuthorDeleted = false; + pm.setAuthor(username.html()); + pm.setAuthorProfileUrl(username.attr("href")); + } + + String date = dateContainer.text(); + date = date.substring(date.indexOf("στις:") + 6, date.indexOf(" »")); + pm.setPmDate(date); + } else { + //Finds username + username = pmContainer.select("a[title^=View the profile of]").first(); + if (username == null) { //Deleted profile + isAuthorDeleted = true; + String authorName = pmContainer + .select("td:has(div.smalltext:containsOwn(Guest))[style^=overflow]") + .first().text(); + authorName = authorName.substring(0, authorName.indexOf(" Guest")); + pm.setAuthor(authorName); + pm.setAuthorColor(ParseHelpers.USER_COLOR_YELLOW); + } else { + isAuthorDeleted = false; + pm.setAuthor(username.html()); + pm.setAuthorProfileUrl(username.attr("href")); + } + + String date = dateContainer.text(); + date = date.substring(date.indexOf("on:") + 4, date.indexOf(" »")); + pm.setPmDate(date); + } + + if (!isAuthorDeleted) { + int postsLineIndex = -1; + int starsLineIndex = -1; + + Element authorInfoContainer = pmContainer.select("div.smalltext").first(); + List infoList = Arrays.asList(authorInfoContainer.html().split("
")); + + if (language == ParseHelpers.Language.GREEK) { + for (String line : infoList) { + if (line.contains("Μηνύματα:")) { + postsLineIndex = infoList.indexOf(line); + //Remove any line breaks and spaces on the start and end + pm.setAuthorNumberOfPosts(line.replace("\n", "").replace("\r", "").trim()); + } + if (line.contains("Φύλο:")) { + if (line.contains("alt=\"Άντρας\"")) + pm.setAuthorGender("Φύλο: Άντρας"); + else + pm.setAuthorGender("Φύλο: Γυναίκα"); + } + if (line.contains("alt=\"*\"")) { + starsLineIndex = infoList.indexOf(line); + Document starsHtml = Jsoup.parse(line); + pm.setAuthorNumberOfStars(starsHtml.select("img[alt]").size()); + pm.setAuthorColor(ParseHelpers.colorPicker(starsHtml.select("img[alt]").first() + .attr("abs:src"))); + } + } + } else { + for (String line : infoList) { + if (line.contains("Posts:")) { + postsLineIndex = infoList.indexOf(line); + //Remove any line breaks and spaces on the start and end + pm.setAuthorNumberOfPosts(line.replace("\n", "").replace("\r", "").trim()); + } + if (line.contains("Gender:")) { + if (line.contains("alt=\"Male\"")) + pm.setAuthorGender("Gender: Male"); + else + pm.setAuthorGender("Gender: Female"); + } + if (line.contains("alt=\"*\"")) { + starsLineIndex = infoList.indexOf(line); + Document starsHtml = Jsoup.parse(line); + pm.setAuthorNumberOfStars(starsHtml.select("img[alt]").size()); + pm.setAuthorColor(ParseHelpers.colorPicker(starsHtml.select("img[alt]").first() + .attr("abs:src"))); + } + } + } + + //If this member has no stars yet ==> New member, + //or is just a member + if (starsLineIndex == -1 || starsLineIndex == 1) { + pm.setAuthorRank(infoList.get(0).trim()); //First line has the rank + //They don't have a special rank + } else if (starsLineIndex == 2) { //This member has a special rank + pm.setAuthorSpecialRank(infoList.get(0).trim());//First line has the special rank + pm.setAuthorRank(infoList.get(1).trim());//Second line has the rank + } + for (int i = postsLineIndex + 1; i < infoList.size() - 1; ++i) { + //Searches under "Posts:" + //and above "Personal Message", "View Profile" etc buttons + String thisLine = infoList.get(i); + if (!Objects.equals(thisLine, "") && thisLine != null + && !Objects.equals(thisLine, " \n") + && !thisLine.contains("avatar") + && !thisLine.contains(" { + if (sessionManager.isLoggedIn()) { + Intent sendPMIntent = new Intent(ProfileActivity.this, CreatePMActivity.class); + sendPMIntent.putExtra(BUNDLE_PROFILE_USERNAME, username); + sendPMIntent.putExtra(CreatePMActivity.BUNDLE_SEND_PM_URL, sendPmUrl); + startActivity(sendPMIntent); + } else { + new AlertDialog.Builder(ProfileActivity.this) + .setMessage("You need to be logged in to send a personal message!") + .setPositiveButton("Login", (dialogInterface, i) -> { + Intent intent = new Intent(ProfileActivity.this, LoginActivity.class); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.push_right_in, R.anim.push_right_out); + }) + .setNegativeButton("Cancel", (dialogInterface, i) -> { + }) + .show(); } }); - }*/ + } ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(Uri.parse(profileUrl)); if (!target.is(ThmmyPage.PageCategory.PROFILE)) { @@ -213,7 +209,7 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment if (pmFAB.getVisibility() != View.GONE) pmFAB.setEnabled(false); } - private void loadAvatar(){ + private void loadAvatar() { Picasso.with(this) .load(avatarUrl) .fit() @@ -226,7 +222,7 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment .into(avatarView); } - private void loadDefaultAvatar(){ + private void loadDefaultAvatar() { Picasso.with(this) .load(R.drawable.ic_default_user_avatar) .fit() @@ -298,6 +294,13 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment usernameSpan.setSpan(new ForegroundColorSpan(Color.parseColor("#26A69A")) , 2, usernameSpan.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + // Url needed to send to send pm + Elements urllinks; + urllinks = profilePage.select("a:contains(Send this member a personal message.)"); + if (urllinks.size() == 0) { + urllinks = profilePage.select("a:contains(Αποστολή προσωπικού μηνύματος σε αυτό το μέλος.)"); + } + sendPmUrl = urllinks.first().attr("href"); return null; } 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 2a9defb9..8ed89ece 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 @@ -1,17 +1,14 @@ package gr.thmmy.mthmmy.activities.topic; -import android.annotation.SuppressLint; import android.app.NotificationManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -21,10 +18,8 @@ import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; -import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -33,7 +28,7 @@ import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.res.ResourcesCompat; -import androidx.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; @@ -54,10 +49,11 @@ import gr.thmmy.mthmmy.model.Bookmark; import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.TopicItem; +import gr.thmmy.mthmmy.pagination.BottomPaginationView; import gr.thmmy.mthmmy.utils.CustomLinearLayoutManager; import gr.thmmy.mthmmy.utils.HTMLUtils; import gr.thmmy.mthmmy.utils.NetworkResultCodes; -import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import gr.thmmy.mthmmy.utils.parsing.StringUtils; import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import timber.log.Timber; @@ -93,35 +89,9 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo //Reply related private FloatingActionButton replyFAB; //Topic's pages related - //Page select related - /** - * Used for handling bottom navigation bar's buttons long click user interactions - */ - private final Handler repeatUpdateHandler = new Handler(); - /** - * Holds the initial time delay before a click on bottom navigation bar is considered long - */ - private final long INITIAL_DELAY = 500; - private boolean autoIncrement = false; - private boolean autoDecrement = false; - /** - * Holds the number of pages to be added or subtracted from current page on each step while a - * long click is held in either next or previous buttons - */ - private static final int SMALL_STEP = 1; - /** - * Holds the number of pages to be added or subtracted from current page on each step while a - * long click is held in either first or last buttons - */ - private static final int LARGE_STEP = 10; //Bottom navigation bar graphics related - private LinearLayout bottomNavBar; - private ImageButton firstPage; - private ImageButton previousPage; - private TextView pageIndicator; - private ImageButton nextPage; - private ImageButton lastPage; + private BottomPaginationView bottomPagination; private Snackbar snackbar; private TopicViewModel viewModel; private EmojiKeyboard emojiKeyboard; @@ -139,7 +109,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo super.onCreate(savedInstanceState); setContentView(R.layout.activity_topic); // get TopicViewModel instance - viewModel = ViewModelProviders.of(this).get(TopicViewModel.class); + viewModel = new ViewModelProvider(this).get(TopicViewModel.class); subscribeUI(); Bundle extras = getIntent().getExtras(); @@ -191,7 +161,6 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo replyFAB = findViewById(R.id.topic_fab); replyFAB.hide(); replyFAB.setTag(false); - bottomNavBar = findViewById(R.id.bottom_navigation_bar); if (!sessionManager.isLoggedIn()) { replyFAB.hide(); replyFAB.setTag(false); @@ -204,18 +173,8 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo } //Sets bottom navigation bar - firstPage = findViewById(R.id.page_first_button); - previousPage = findViewById(R.id.page_previous_button); - pageIndicator = findViewById(R.id.page_indicator); - nextPage = findViewById(R.id.page_next_button); - lastPage = findViewById(R.id.page_last_button); - - initDecrementButton(firstPage, LARGE_STEP); - initDecrementButton(previousPage, SMALL_STEP); - initIncrementButton(nextPage, SMALL_STEP); - initIncrementButton(lastPage, LARGE_STEP); - - paginationEnabled(false); + bottomPagination = findViewById(R.id.bottom_pagination); + bottomPagination.setOnPageRequestedListener(viewModel); Timber.i("Starting initial topic load"); viewModel.loadUrl(topicPageUrl); @@ -291,7 +250,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo viewModel.setWritingReply(false); replyFAB.show(); replyFAB.setTag(true); - bottomNavBar.setVisibility(View.VISIBLE); + bottomPagination.setVisibility(View.VISIBLE); return; } else if (viewModel.isEditingPost()) { ((Post) topicItems.get(viewModel.getPostBeingEditedPosition())).setPostType(Post.TYPE_POST); @@ -300,7 +259,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo viewModel.setEditingPost(false); replyFAB.show(); replyFAB.setTag(true); - bottomNavBar.setVisibility(View.VISIBLE); + bottomPagination.setVisibility(View.VISIBLE); return; } super.onBackPressed(); @@ -340,152 +299,6 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo recyclerView.scrollToPosition(position); } - //--------------------------------------BOTTOM NAV BAR METHODS---------------------------------- - - /** - * This class is used to implement the repetitive incrementPageRequestValue/decrementPageRequestValue - * of page value when long pressing one of the page navigation buttons. - */ - private class RepetitiveUpdater implements Runnable { - private final int step; - - /** - * @param step number of pages to add/subtract on each repetition - */ - RepetitiveUpdater(int step) { - this.step = step; - } - - public void run() { - long REPEAT_DELAY = 250; - if (autoIncrement) { - viewModel.incrementPageRequestValue(step, false); - repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), REPEAT_DELAY); - } else if (autoDecrement) { - viewModel.decrementPageRequestValue(step, false); - repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), REPEAT_DELAY); - } - } - } - - private void paginationEnabled(boolean enabled) { - firstPage.setEnabled(enabled); - previousPage.setEnabled(enabled); - nextPage.setEnabled(enabled); - lastPage.setEnabled(enabled); - } - - private void paginationDisable(View exception) { - if (exception == firstPage) { - previousPage.setEnabled(false); - nextPage.setEnabled(false); - lastPage.setEnabled(false); - } else if (exception == previousPage) { - firstPage.setEnabled(false); - nextPage.setEnabled(false); - lastPage.setEnabled(false); - } else if (exception == nextPage) { - firstPage.setEnabled(false); - previousPage.setEnabled(false); - lastPage.setEnabled(false); - } else if (exception == lastPage) { - firstPage.setEnabled(false); - previousPage.setEnabled(false); - nextPage.setEnabled(false); - } else { - paginationEnabled(false); - } - } - - @SuppressLint("ClickableViewAccessibility") - private void initIncrementButton(ImageButton increment, final int step) { - // Increment once for a click - increment.setOnClickListener(v -> { - if (!autoIncrement && step == LARGE_STEP) { - viewModel.setPageIndicatorIndex(viewModel.getPageCount(), true); - } else if (!autoIncrement) { - viewModel.incrementPageRequestValue(step, true); - } - }); - - // Auto increment for a long click - increment.setOnLongClickListener( - arg0 -> { - paginationDisable(arg0); - autoIncrement = true; - repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); - return false; - } - ); - - // When the button is released - increment.setOnTouchListener(new View.OnTouchListener() { - private Rect rect; - - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); - } else if (rect != null && event.getAction() == MotionEvent.ACTION_UP && autoIncrement) { - autoIncrement = false; - paginationEnabled(true); - viewModel.loadPageIndicated(); - } else if (rect != null && event.getAction() == MotionEvent.ACTION_MOVE) { - if (!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) { - autoIncrement = false; - viewModel.setPageIndicatorIndex(viewModel.getCurrentPageIndex(), false); - paginationEnabled(true); - } - } - return false; - } - }); - } - - @SuppressLint("ClickableViewAccessibility") - private void initDecrementButton(ImageButton decrement, final int step) { - // Decrement once for a click - decrement.setOnClickListener(v -> { - if (!autoDecrement && step == LARGE_STEP) { - viewModel.setPageIndicatorIndex(1, true); - } else if (!autoDecrement) { - viewModel.decrementPageRequestValue(step, true); - } - }); - - // Auto decrement for a long click - decrement.setOnLongClickListener( - arg0 -> { - paginationDisable(arg0); - autoDecrement = true; - repeatUpdateHandler.postDelayed(new RepetitiveUpdater(step), INITIAL_DELAY); - return false; - } - ); - - // When the button is released - decrement.setOnTouchListener(new View.OnTouchListener() { - private Rect rect; - - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); - } else if (event.getAction() == MotionEvent.ACTION_UP && autoDecrement) { - autoDecrement = false; - paginationEnabled(true); - viewModel.loadPageIndicated(); - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (rect != null && - !rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) { - autoIncrement = false; - viewModel.setPageIndicatorIndex(viewModel.getCurrentPageIndex(), false); - paginationEnabled(true); - } - } - return false; - } - }); - } - //------------------------------------BOTTOM NAV BAR METHODS END------------------------------------ /** @@ -538,7 +351,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo Timber.i("Post reply successful"); replyFAB.show(); replyFAB.setTag(true); - bottomNavBar.setVisibility(View.VISIBLE); + bottomPagination.setVisibility(View.VISIBLE); viewModel.setWritingReply(false); SharedPreferences drafts = getSharedPreferences(getString(R.string.pref_topic_drafts_key), @@ -547,7 +360,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo if ((((Post) topicItems.get(topicItems.size() - 1)).getPostNumber() + 1) % 15 == 0) { Timber.i("Reply was posted in new page. Switching to last page."); - viewModel.loadUrl(ParseHelpers.getBaseURL(viewModel.getTopicUrl()) + "." + 2147483647); + viewModel.loadUrl(StringUtils.getBaseURL(viewModel.getTopicUrl()) + "." + 2147483647); } else { viewModel.reloadPage(); } @@ -615,7 +428,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo topicAdapter.notifyItemChanged(position); replyFAB.show(); replyFAB.setTag(true); - bottomNavBar.setVisibility(View.VISIBLE); + bottomPagination.setVisibility(View.VISIBLE); viewModel.setEditingPost(false); viewModel.reloadPage(); } else { @@ -661,11 +474,14 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo Toast.makeText(this, "Failed to remove vote", Toast.LENGTH_LONG).show(); } }); - // observe the chages in data + // observe the changes in data viewModel.getPageIndicatorIndex().observe(this, pageIndicatorIndex -> { if (pageIndicatorIndex == null) return; - pageIndicator.setText(String.valueOf(pageIndicatorIndex) + "/" + - String.valueOf(viewModel.getPageCount())); + bottomPagination.setIndicatedPageIndex(pageIndicatorIndex); + }); + viewModel.getPageCount().observe(this, pageCount -> { + if (pageCount == null) return; + bottomPagination.setTotalPageCount(pageCount); }); viewModel.getTopicTitle().observe(this, newTopicTitle -> { if (newTopicTitle == null) return; @@ -674,7 +490,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo }); viewModel.getPageTopicId().observe(this, pageTopicId -> { if (pageTopicId == null) return; - if (viewModel.getCurrentPageIndex() == viewModel.getPageCount()) { + if (viewModel.getCurrentPageIndex() == viewModel.getPageCount().getValue()) { NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager != null) notificationManager.cancel(NEW_POST_TAG, pageTopicId); @@ -696,6 +512,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo topicItems.addAll(postList); topicAdapter.notifyDataSetChanged(); }); + // Scroll to position does not work because WebView size is unknown initially /*viewModel.getFocusedPostIndex().observe(this, focusedPostIndex -> { if (focusedPostIndex == null) return; recyclerView.scrollToPosition(focusedPostIndex); @@ -706,7 +523,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo switch (resultCode) { case SUCCESS: Timber.i("Successfully loaded a topic"); - paginationEnabled(true); + bottomPagination.setEnabled(true); break; case NETWORK_ERROR: Timber.w("Network error on loaded page"); @@ -731,7 +548,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo }); } else { // a page has already been loaded - viewModel.setPageIndicatorIndex(viewModel.getCurrentPageIndex(), false); + bottomPagination.setIndicatedPageIndex(viewModel.getCurrentPageIndex()); snackbar = Snackbar.make(findViewById(R.id.main_content), R.string.generic_network_error, Snackbar.LENGTH_INDEFINITE); snackbar.setAction(R.string.retry, view -> viewModel.reloadPage()); @@ -773,7 +590,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo recyclerView.scrollToPosition(topicItems.size() - 1); replyFAB.hide(); replyFAB.setTag(false); - bottomNavBar.setVisibility(View.GONE); + bottomPagination.setVisibility(View.GONE); } else { Timber.i("Prepare for reply unsuccessful"); Snackbar.make(findViewById(R.id.main_content), getString(R.string.generic_network_error), Snackbar.LENGTH_SHORT).show(); @@ -789,7 +606,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo recyclerView.scrollToPosition(result.getPosition()); replyFAB.hide(); replyFAB.setTag(false); - bottomNavBar.setVisibility(View.GONE); + bottomPagination.setVisibility(View.GONE); } else { Timber.i("Prepare for edit unsuccessful"); Snackbar.make(findViewById(R.id.main_content), getString(R.string.generic_network_error), Snackbar.LENGTH_SHORT).show(); @@ -797,9 +614,11 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo }); } - /**This method sets a long click listener on the title of the topic. Once the + /** + * This method sets a long click listener on the title of the topic. Once the * listener gets triggered, it copies the link url of the topic in the clipboard. - * This method is getting called on the onCreate() of the TopicActivity*/ + * This method is getting called on the onCreate() of the TopicActivity + */ void setToolbarOnLongClickListener(String url) { toolbar.setOnLongClickListener(view -> { //Try to set the clipboard text 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 0e885cc3..1edcd3a5 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 @@ -73,7 +73,9 @@ import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.TopicItem; import gr.thmmy.mthmmy.utils.CircleTransform; +import gr.thmmy.mthmmy.utils.MessageAnimations; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import gr.thmmy.mthmmy.utils.parsing.StringUtils; import gr.thmmy.mthmmy.utils.parsing.ThmmyParser; import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import timber.log.Timber; @@ -85,8 +87,8 @@ import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_ 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.TopicActivity.BUNDLE_TOPIC_URL; -import static gr.thmmy.mthmmy.activities.topic.TopicParser.USER_COLOR_WHITE; -import static gr.thmmy.mthmmy.activities.topic.TopicParser.USER_COLOR_YELLOW; +import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.USER_COLOR_WHITE; +import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.USER_COLOR_YELLOW; import static gr.thmmy.mthmmy.base.BaseActivity.getSessionManager; import static gr.thmmy.mthmmy.utils.FileUtils.faIconFromFilename; @@ -495,7 +497,7 @@ class TopicAdapter extends RecyclerView.Adapter { } else //noinspection deprecation holder.cardChildLinear.setBackground(context.getResources(). getDrawable(R.drawable.mention_card)); - } else if (mUserColor == TopicParser.USER_COLOR_PINK) { + } else if (mUserColor == ParseHelpers.USER_COLOR_PINK) { //Special card for special member of the month! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { holder.cardChildLinear.setBackground(context.getResources(). @@ -546,14 +548,14 @@ class TopicAdapter extends RecyclerView.Adapter { holder.header.setOnClickListener(v -> { //Clicking the header makes it expand/collapse viewModel.toggleUserInfo(holder.getAdapterPosition()); - TopicAnimations.animateUserExtraInfoVisibility(holder.username, + MessageAnimations.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(v -> { viewModel.hideUserInfo(holder.getAdapterPosition()); - TopicAnimations.animateUserExtraInfoVisibility(holder.username, + MessageAnimations.animateUserExtraInfoVisibility(holder.username, holder.subject, Color.parseColor("#FFFFFF"), Color.parseColor("#757575"), (LinearLayout) v); }); @@ -964,9 +966,9 @@ class TopicAdapter extends RecyclerView.Adapter { if (target.is(ThmmyPage.PageCategory.TOPIC)) { //This url points to a topic //Checks if the page to be loaded is the one already shown - if (uriString.contains(ParseHelpers.getBaseURL(viewModel.getTopicUrl()))) { + if (uriString.contains(StringUtils.getBaseURL(viewModel.getTopicUrl()))) { if (uriString.contains("topicseen#new") || uriString.contains("#new")) { - if (viewModel.getCurrentPageIndex() == viewModel.getPageCount()) { + if (viewModel.getCurrentPageIndex() == viewModel.getPageCount().getValue()) { //same page postFocusListener.onPostFocusChange(getItemCount() - 1); Timber.e("new"); @@ -986,9 +988,9 @@ class TopicAdapter extends RecyclerView.Adapter { return true; } } - } else if ((Objects.equals(uriString, ParseHelpers.getBaseURL(viewModel.getTopicUrl())) && + } else if ((Objects.equals(uriString, StringUtils.getBaseURL(viewModel.getTopicUrl())) && viewModel.getCurrentPageIndex() == 1) || - Integer.parseInt(uriString.substring(ParseHelpers.getBaseURL(viewModel.getTopicUrl()).length() + 1)) / 15 + 1 == + Integer.parseInt(uriString.substring(StringUtils.getBaseURL(viewModel.getTopicUrl()).length() + 1)) / 15 + 1 == viewModel.getCurrentPageIndex()) { //same page return true; 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 1a7922ff..bd8dfab2 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 @@ -1,7 +1,5 @@ package gr.thmmy.mthmmy.activities.topic; -import android.graphics.Color; - import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -30,23 +28,9 @@ import timber.log.Timber; * Singleton used for parsing a topic. *

Class contains the methods: