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:{@link #parseUsersViewingThisTopic(Document, * ParseHelpers.Language)} - * {@link #parseCurrentPageIndex(Document, ParseHelpers.Language)} - * {@link #parseTopicNumberOfPages(Document, int, ParseHelpers.Language)} * {@link #parseTopic(Document, ParseHelpers.Language)} */ public class TopicParser { - private static Pattern mentionsPattern = Pattern. - compile("\\n\\s+?(Quote from|Παράθεση από): " - + BaseActivity.getSessionManager().getUsername() +"\\s(στις|on)"); - - //User colors - private static final int USER_COLOR_BLACK = Color.parseColor("#000000"); - private static final int USER_COLOR_RED = Color.parseColor("#F44336"); - private static final int USER_COLOR_GREEN = Color.parseColor("#4CAF50"); - private static final int USER_COLOR_BLUE = Color.parseColor("#536DFE"); - static final int USER_COLOR_PINK = Color.parseColor("#FF4081"); - static final int USER_COLOR_YELLOW = Color.parseColor("#FFEB3B"); - static final int USER_COLOR_WHITE = Color.WHITE; /** * Returns users currently viewing this topic. @@ -64,82 +48,6 @@ public class TopicParser { return topic.select("td:containsOwn(are viewing this topic)").first().html(); } - /** - * Returns current topic's page index. - * - * @param topic {@link Document} object containing this topic's source code - * @param language a {@link ParseHelpers.Language} containing this topic's - * language set, this is returned by - * {@link ParseHelpers.Language#getLanguage(Document)} - * @return int containing parsed topic's current page - * @see org.jsoup.Jsoup Jsoup - */ - public static int parseCurrentPageIndex(Document topic, ParseHelpers.Language language) { - int parsedPage = 1; - - if (language == ParseHelpers.Language.GREEK) { - Elements findCurrentPage = topic.select("td:contains(Σελίδες:)>b"); - - for (Element item : findCurrentPage) { - if (!item.text().contains("...") - && !item.text().contains("Σελίδες:")) { - parsedPage = Integer.parseInt(item.text()); - break; - } - } - } else { - Elements findCurrentPage = topic.select("td:contains(Pages:)>b"); - - for (Element item : findCurrentPage) { - if (!item.text().contains("...") && !item.text().contains("Pages:")) { - parsedPage = Integer.parseInt(item.text()); - break; - } - } - } - - return parsedPage; - } - - /** - * Returns the number of this topic's pages. - * - * @param topic {@link Document} object containing this topic's source code - * @param currentPage an int containing current page of this topic - * @param language a {@link ParseHelpers.Language} containing this topic's - * language set, this is returned by - * {@link ParseHelpers.Language#getLanguage(Document)} - * @return int containing the number of pages - * @see org.jsoup.Jsoup Jsoup - */ - public static int parseTopicNumberOfPages(Document topic, int currentPage, ParseHelpers.Language language) { - int returnPages = 1; - - if (language == ParseHelpers.Language.GREEK) { - Elements pages = topic.select("td:contains(Σελίδες:)>a.navPages"); - - if (pages.size() != 0) { - returnPages = currentPage; - for (Element item : pages) { - if (Integer.parseInt(item.text()) > returnPages) - returnPages = Integer.parseInt(item.text()); - } - } - } else { - Elements pages = topic.select("td:contains(Pages:)>a.navPages"); - - if (pages.size() != 0) { - returnPages = currentPage; - for (Element item : pages) { - if (Integer.parseInt(item.text()) > returnPages) - returnPages = Integer.parseInt(item.text()); - } - } - } - - return returnPages; - } - /** * This method parses all the information of a topic and it's posts. * @@ -186,7 +94,7 @@ public class TopicParser { p_personalText = ""; p_numberOfPosts = ""; p_numberOfStars = 0; - p_userColor = USER_COLOR_YELLOW; + p_userColor = ParseHelpers.USER_COLOR_YELLOW; p_attachedFiles = new ArrayList<>(); p_postLastEditDate = null; p_deletePostURL = null; @@ -245,7 +153,7 @@ public class TopicParser { .select("td:has(div.smalltext:containsOwn(Επισκέπτης))[style^=overflow]") .first().text(); p_userName = p_userName.substring(0, p_userName.indexOf(" Επισκέπτης")); - p_userColor = USER_COLOR_YELLOW; + p_userColor = ParseHelpers.USER_COLOR_YELLOW; } else { p_userName = userName.html(); p_profileURL = userName.attr("href"); @@ -315,7 +223,7 @@ public class TopicParser { .select("td:has(div.smalltext:containsOwn(Guest))[style^=overflow]") .first().text(); p_userName = p_userName.substring(0, p_userName.indexOf(" Guest")); - p_userColor = USER_COLOR_YELLOW; + p_userColor = ParseHelpers.USER_COLOR_YELLOW; } else { p_userName = userName.html(); p_profileURL = userName.attr("href"); @@ -405,7 +313,7 @@ public class TopicParser { starsLineIndex = infoList.indexOf(line); Document starsHtml = Jsoup.parse(line); p_numberOfStars = starsHtml.select("img[alt]").size(); - p_userColor = colorPicker(starsHtml.select("img[alt]").first() + p_userColor = ParseHelpers.colorPicker(starsHtml.select("img[alt]").first() .attr("abs:src")); } } @@ -426,7 +334,7 @@ public class TopicParser { starsLineIndex = infoList.indexOf(line); Document starsHtml = Jsoup.parse(line); p_numberOfStars = starsHtml.select("img[alt]").size(); - p_userColor = colorPicker(starsHtml.select("img[alt]").first() + p_userColor = ParseHelpers.colorPicker(starsHtml.select("img[alt]").first() .attr("abs:src")); } } @@ -456,7 +364,7 @@ public class TopicParser { //Checks post for mentions of this user (if the user is logged in) if (BaseActivity.getSessionManager().isLoggedIn() && - mentionsPattern.matcher(p_post).find()) { + ParseHelpers.mentionsPattern.matcher(p_post).find()) { p_isUserMentionedInPost = true; } @@ -567,25 +475,4 @@ public class TopicParser { return null; } - /** - * Returns the color of a user according to user's rank on forum. - * - * @param starsUrl String containing the URL of a user's stars - * @return an int corresponding to the right color - */ - private static int colorPicker(String starsUrl) { - if (starsUrl.contains("/star.gif")) - return USER_COLOR_YELLOW; - else if (starsUrl.contains("/starmod.gif")) - return USER_COLOR_GREEN; - else if (starsUrl.contains("/stargmod.gif")) - return USER_COLOR_BLUE; - else if (starsUrl.contains("/staradmin.gif")) - return USER_COLOR_RED; - else if (starsUrl.contains("/starweb.gif")) - return USER_COLOR_BLACK; - else if (starsUrl.contains("/oscar.gif")) - return USER_COLOR_PINK; - return USER_COLOR_YELLOW; - } } 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 index 23b3e602..b28f19e7 100644 --- 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 @@ -97,10 +97,10 @@ public class TopicTask extends AsyncTask { , topicTitle.indexOf("(Αναγνώστηκε") - 2); //Finds current page's index - int currentPageIndex = TopicParser.parseCurrentPageIndex(topic, language); + int currentPageIndex = ParseHelpers.parseCurrentPageIndex(topic, language); //Finds number of pages - int pageCount = TopicParser.parseTopicNumberOfPages(topic, currentPageIndex, language); + int pageCount = ParseHelpers.parseNumberOfPages(topic, currentPageIndex, language); ArrayList newPostsList = TopicParser.parseTopic(topic, language); 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 22357d76..d9fdab52 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java @@ -56,6 +56,7 @@ import gr.thmmy.mthmmy.activities.AboutActivity; import gr.thmmy.mthmmy.activities.LoginActivity; import gr.thmmy.mthmmy.activities.bookmarks.BookmarksActivity; import gr.thmmy.mthmmy.activities.downloads.DownloadsActivity; +import gr.thmmy.mthmmy.activities.inbox.InboxActivity; import gr.thmmy.mthmmy.activities.main.MainActivity; import gr.thmmy.mthmmy.activities.profile.ProfileActivity; import gr.thmmy.mthmmy.activities.settings.SettingsActivity; @@ -196,6 +197,7 @@ public abstract class BaseActivity extends AppCompatActivity { protected static final int ABOUT_ID = 5; protected static final int SETTINGS_ID = 6; protected static final int SHOUTBOX_ID = 7; + protected static final int INBOX_ID = 8; private AccountHeader accountHeader; private ProfileDrawerItem profileDrawerItem; @@ -210,8 +212,9 @@ public abstract class BaseActivity extends AppCompatActivity { final int selectedPrimaryColor = ContextCompat.getColor(this, R.color.primary_dark); final int selectedSecondaryColor = ContextCompat.getColor(this, R.color.accent); - PrimaryDrawerItem homeItem, bookmarksItem, settingsItem, aboutItem, shoutboxItem; - IconicsDrawable homeIcon, homeIconSelected, downloadsIcon, downloadsIconSelected, uploadIcon, uploadIconSelected, settingsIcon, + PrimaryDrawerItem homeItem, bookmarksItem, settingsItem, aboutItem, shoutboxItem, inboxItem; + IconicsDrawable homeIcon, homeIconSelected, downloadsIcon, downloadsIconSelected, uploadIcon, + uploadIconSelected, settingsIcon, settingsIconSelected, bookmarksIcon, bookmarksIconSelected, aboutIcon, aboutIconSelected; //Drawer Icons @@ -310,6 +313,18 @@ public abstract class BaseActivity extends AppCompatActivity { .withSelectedIconColor(selectedSecondaryColor) .withIconTintingEnabled(true); + inboxItem = new PrimaryDrawerItem() + .withTextColor(primaryColor) + .withSelectedColor(selectedPrimaryColor) + .withSelectedTextColor(selectedSecondaryColor) + .withIdentifier(INBOX_ID) + .withName(R.string.inbox) + .withIcon(R.drawable.ic_message_white_24dp) + .withIconColor(primaryColor) + .withSelectedIconColor(selectedSecondaryColor) + .withIconTintingEnabled(true); + + if (sessionManager.isLoggedIn()) //When logged in { loginLogoutItem = new PrimaryDrawerItem() @@ -404,6 +419,11 @@ public abstract class BaseActivity extends AppCompatActivity { Intent intent = new Intent(BaseActivity.this, ShoutboxActivity.class); startActivity(intent); } + } else if (drawerItem.equals(INBOX_ID)) { + if (!(BaseActivity.this instanceof InboxActivity)) { + Intent intent = new Intent(BaseActivity.this, InboxActivity.class); + startActivity(intent); + } } else if (drawerItem.equals(DOWNLOADS_ID)) { if (!(BaseActivity.this instanceof DownloadsActivity)) { Intent intent = new Intent(BaseActivity.this, DownloadsActivity.class); @@ -448,7 +468,7 @@ public abstract class BaseActivity extends AppCompatActivity { }); if (sessionManager.isLoggedIn()) - drawerBuilder.addDrawerItems(homeItem, bookmarksItem, shoutboxItem, downloadsItem, settingsItem, loginLogoutItem, aboutItem); + drawerBuilder.addDrawerItems(homeItem, bookmarksItem, shoutboxItem, inboxItem, downloadsItem, settingsItem, loginLogoutItem, aboutItem); else drawerBuilder.addDrawerItems(homeItem, bookmarksItem, shoutboxItem, settingsItem, loginLogoutItem, aboutItem); diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/Inbox.java b/app/src/main/java/gr/thmmy/mthmmy/model/Inbox.java new file mode 100644 index 00000000..06cc9777 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/model/Inbox.java @@ -0,0 +1,32 @@ +package gr.thmmy.mthmmy.model; + +import java.util.ArrayList; + +public class Inbox { + private ArrayList pms; + private int currentPageIndex, numberOfPages; + + public int getCurrentPageIndex() { + return currentPageIndex; + } + + public void setCurrentPageIndex(int currentPageIndex) { + this.currentPageIndex = currentPageIndex; + } + + public int getNumberOfPages() { + return numberOfPages; + } + + public void setNumberOfPages(int numberOfPages) { + this.numberOfPages = numberOfPages; + } + + public ArrayList getPms() { + return pms; + } + + public void setPms(ArrayList pms) { + this.pms = pms; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/PM.java b/app/src/main/java/gr/thmmy/mthmmy/model/PM.java new file mode 100644 index 00000000..a4ffcd8d --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/model/PM.java @@ -0,0 +1,157 @@ +package gr.thmmy.mthmmy.model; + +import gr.thmmy.mthmmy.utils.parsing.StringUtils; +import gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser; + +public class PM { + + private String thumbnailUrl; + private String author; + private String authorProfileUrl; + private String subject; + private String content; + private String pmDate; + private String deleteUrl, replyUrl, quoteUrl; + private int authorColor; + private String authorGender; + private String authorNumberOfPosts; + private String authorRank, authorSpecialRank, authorPersonalText; + private int authorNumberOfStars; + private boolean isUserMentioned; + + public int getAuthorColor() { + return authorColor; + } + + public String getAuthorPersonalText() { + return authorPersonalText; + } + + public void setAuthorPersonalText(String authorPersonalText) { + this.authorPersonalText = authorPersonalText; + } + + public String getAuthorNumberOfPosts() { + return authorNumberOfPosts; + } + + public String getAuthorRank() { + return authorRank; + } + + public void setAuthorRank(String rank) { + this.authorRank = rank; + } + + public String getAuthorSpecialRank() { + return authorSpecialRank; + } + + public void setAuthorSpecialRank(String authorSpecialRank) { + this.authorSpecialRank = authorSpecialRank; + } + + public String getAuthorGender() { + return authorGender; + } + + public void setAuthorGender(String authorGender) { + this.authorGender = authorGender; + } + + public int getAuthorNumberOfStars() { + return authorNumberOfStars; + } + + public void setAuthorNumberOfStars(int authorNumberOfStars) { + this.authorNumberOfStars = authorNumberOfStars; + } + + public void setAuthorNumberOfPosts(String authorNumberOfPosts) { + this.authorNumberOfPosts = authorNumberOfPosts; + } + + public String getReplyUrl() { + return replyUrl; + } + + public void setReplyUrl(String replyUrl) { + this.replyUrl = replyUrl; + } + + public String getQuoteUrl() { + return quoteUrl; + } + + public void setQuoteUrl(String quoteUrl) { + this.quoteUrl = quoteUrl; + } + + public String getDeleteUrl() { + return deleteUrl; + } + + public void setDeleteUrl(String deleteUrl) { + this.deleteUrl = deleteUrl; + } + + public void setAuthorColor(int authorColor) { + this.authorColor = authorColor; + } + + public boolean isUserMentioned() { + return isUserMentioned; + } + + public void setUserMentioned(boolean userMentioned) { + isUserMentioned = userMentioned; + } + + public String getAuthorProfileUrl() { + return authorProfileUrl; + } + + public void setAuthorProfileUrl(String authorProfileUrl) { + this.authorProfileUrl = authorProfileUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } + + public void setAuthor(String author) { + this.author = author; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public void setContent(String content) { + this.content = content; + } + + public void setPmDate(String pmDate) { + this.pmDate = pmDate; + } + + public String getAuthor() { + return author; + } + + public String getContent() { + return content; + } + + public String getSubject() { + return subject; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public String getPmDate() { + return pmDate; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/pagination/BottomPaginationView.java b/app/src/main/java/gr/thmmy/mthmmy/pagination/BottomPaginationView.java new file mode 100644 index 00000000..9accfeb7 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/pagination/BottomPaginationView.java @@ -0,0 +1,263 @@ +package gr.thmmy.mthmmy.pagination; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Rect; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import gr.thmmy.mthmmy.R; + +public class BottomPaginationView extends LinearLayout { + + /** + * 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; + /** + * Used for handling bottom navigation bar's buttons long click user interactions + */ + private final Handler repeatUpdateHandler = new Handler(); + + private ImageButton firstPage, previousPage, nextPage, lastPage; + private TextView pageIndicator; + + private int onDownPageIndex, indicatorPageIndex, totalPageCount; + + private OnPageRequestedListener onPageRequestedListener; + + public interface OnPageRequestedListener { + void onPageRequested(int index); + } + + public BottomPaginationView(Context context) { + this(context, null); + } + + public BottomPaginationView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + LayoutInflater inflater = LayoutInflater.from(context); + inflater.inflate(R.layout.pagination, this, true); + + 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); + } + + public void setOnPageRequestedListener(OnPageRequestedListener onPageRequestedListener) { + this.onPageRequestedListener = onPageRequestedListener; + } + + public boolean setIndicatedPageIndex(int index) { + if (index != indicatorPageIndex) { + this.indicatorPageIndex = index; + updateUI(); + return true; + } else return false; + } + + public void setTotalPageCount(int totalPageCount) { + this.totalPageCount = totalPageCount; + updateUI(); + } + + public void updateUI() { + pageIndicator.setText(indicatorPageIndex + "/" + totalPageCount); + } + + /** + * 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) { + incrementPageIndicator(step); + repeatUpdateHandler.postDelayed(new BottomPaginationView.RepetitiveUpdater(step), REPEAT_DELAY); + } else if (autoDecrement) { + decrementPageIndicator(step); + repeatUpdateHandler.postDelayed(new BottomPaginationView.RepetitiveUpdater(step), REPEAT_DELAY); + } + } + } + + public boolean incrementPageIndicator(int step) { + int oldIndicatorIndex = indicatorPageIndex; + if (oldIndicatorIndex <= totalPageCount - step) + setIndicatedPageIndex(indicatorPageIndex + step); + else + setIndicatedPageIndex(totalPageCount); + return oldIndicatorIndex != indicatorPageIndex; + } + + public boolean decrementPageIndicator(int step) { + int oldIndicatorIndex = indicatorPageIndex; + if (oldIndicatorIndex > step) + setIndicatedPageIndex(indicatorPageIndex - step); + else + setIndicatedPageIndex(1); + return oldIndicatorIndex != indicatorPageIndex; + } + + @SuppressLint("ClickableViewAccessibility") + private void initIncrementButton(ImageButton increment, final int step) { + // Increment once for a click + increment.setOnClickListener(v -> { + if (!autoIncrement && step == LARGE_STEP) { + boolean indicatorChanged = setIndicatedPageIndex(totalPageCount); + if (indicatorChanged) onPageRequestedListener.onPageRequested(indicatorPageIndex); + } else if (!autoIncrement) { + boolean indicatorChanged = incrementPageIndicator(1); + if (indicatorChanged) onPageRequestedListener.onPageRequested(indicatorPageIndex); + } + }); + + // Auto increment for a long click + increment.setOnLongClickListener( + arg0 -> { + paginationDisable(arg0); + autoIncrement = true; + repeatUpdateHandler.postDelayed(new BottomPaginationView.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) { + onDownPageIndex = indicatorPageIndex; + 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); + onPageRequestedListener.onPageRequested(indicatorPageIndex); + } else if (rect != null && event.getAction() == MotionEvent.ACTION_MOVE) { + if (!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) { + autoIncrement = false; + setIndicatedPageIndex(onDownPageIndex); + 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) { + boolean indicatorChanged = setIndicatedPageIndex(1); + if (indicatorChanged) onPageRequestedListener.onPageRequested(indicatorPageIndex); + } else if (!autoDecrement) { + boolean indicatorChanged = decrementPageIndicator(1); + if (indicatorChanged) onPageRequestedListener.onPageRequested(indicatorPageIndex); + } + }); + + // 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) { + onDownPageIndex = indicatorPageIndex; + rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); + } else if (event.getAction() == MotionEvent.ACTION_UP && autoDecrement) { + autoDecrement = false; + paginationEnabled(true); + onPageRequestedListener.onPageRequested(indicatorPageIndex); + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (rect != null && + !rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) { + autoIncrement = false; + setIndicatedPageIndex(onDownPageIndex); + paginationEnabled(true); + } + } + return false; + } + }); + } + + public 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); + } + } + + public void paginationEnabled(boolean enabled) { + firstPage.setEnabled(enabled); + previousPage.setEnabled(enabled); + nextPage.setEnabled(enabled); + lastPage.setEnabled(enabled); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java b/app/src/main/java/gr/thmmy/mthmmy/utils/MessageAnimations.java similarity index 94% rename from app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java rename to app/src/main/java/gr/thmmy/mthmmy/utils/MessageAnimations.java index 4cabbdfb..236d042c 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/MessageAnimations.java @@ -1,4 +1,4 @@ -package gr.thmmy.mthmmy.activities.topic; +package gr.thmmy.mthmmy.utils; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -7,11 +7,11 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; -class TopicAnimations { +public class MessageAnimations { /** * Method that animates view's visibility changes for user's extra info */ - static void animateUserExtraInfoVisibility(final TextView username, final TextView subject, + public static void animateUserExtraInfoVisibility(final TextView username, final TextView subject, int expandedColor, final int collapsedColor, final LinearLayout userExtraInfo) { //If the view is currently gone it fades it in diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java b/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java index 206d4c3b..7f9f67d1 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java @@ -1,5 +1,7 @@ package gr.thmmy.mthmmy.utils.parsing; +import android.graphics.Color; + import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -11,15 +13,50 @@ import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import gr.thmmy.mthmmy.base.BaseActivity; import timber.log.Timber; /** * This class consists exclusively of static classes (enums) and methods (excluding methods of inner - * classes). It can be used to resolve a page's language and state or fix embedded videos html code + * classes). It can be used to resolve a page's language, number of pages and state or fix embedded videos html code * and obfuscated emails. */ public class ParseHelpers { + public static final int USER_COLOR_PINK = Color.parseColor("#FF4081"); + public static final int USER_COLOR_YELLOW = Color.parseColor("#FFEB3B"); + public static final int USER_COLOR_WHITE = Color.WHITE; + //User colors + private static final int USER_COLOR_BLACK = Color.parseColor("#000000"); + private static final int USER_COLOR_RED = Color.parseColor("#F44336"); + private static final int USER_COLOR_GREEN = Color.parseColor("#4CAF50"); + private static final int USER_COLOR_BLUE = Color.parseColor("#536DFE"); + public static Pattern mentionsPattern = Pattern. + compile("\\n\\s+?(Quote from|Παράθεση από): " + + BaseActivity.getSessionManager().getUsername() +"\\s(στις|on)"); + + /** + * Returns the color of a user according to user's rank on forum. + * + * @param starsUrl String containing the URL of a user's stars + * @return an int corresponding to the right color + */ + public static int colorPicker(String starsUrl) { + if (starsUrl.contains("/star.gif")) + return USER_COLOR_YELLOW; + else if (starsUrl.contains("/starmod.gif")) + return USER_COLOR_GREEN; + else if (starsUrl.contains("/stargmod.gif")) + return USER_COLOR_BLUE; + else if (starsUrl.contains("/staradmin.gif")) + return USER_COLOR_RED; + else if (starsUrl.contains("/starweb.gif")) + return USER_COLOR_BLACK; + else if (starsUrl.contains("/oscar.gif")) + return USER_COLOR_PINK; + return USER_COLOR_YELLOW; + } + /** * An enum describing a forum page's language by defining the types: * {@link #PAGE_INCOMPLETE} @@ -172,20 +209,134 @@ public class ParseHelpers { } /** - * Method that extracts the base URL from a topic's page 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" + * Returns the number of this page's pages. + * + * @param topic {@link Document} object containing this page's source code + * @param currentPage an int containing current page of this page + * @param language a {@link ParseHelpers.Language} containing this topic's + * language set, this is returned by + * {@link ParseHelpers.Language#getLanguage(Document)} + * @return int containing the number of pages + * @see org.jsoup.Jsoup Jsoup + */ + public static int parseNumberOfPages(Document topic, int currentPage, ParseHelpers.Language language) { + int returnPages = 1; + + if (language == ParseHelpers.Language.GREEK) { + Elements pages = topic.select("td:contains(Σελίδες:)>a.navPages"); + + if (pages.size() != 0) { + returnPages = currentPage; + for (Element item : pages) { + if (Integer.parseInt(item.text()) > returnPages) + returnPages = Integer.parseInt(item.text()); + } + } + } else { + Elements pages = topic.select("td:contains(Pages:)>a.navPages"); + + if (pages.size() != 0) { + returnPages = currentPage; + for (Element item : pages) { + if (Integer.parseInt(item.text()) > returnPages) + returnPages = Integer.parseInt(item.text()); + } + } + } + + return returnPages; + } + + public static int parseNumberOfPagesInbox(Document topic, int currentPage, ParseHelpers.Language language) { + int returnPages = 1; + + if (language == ParseHelpers.Language.GREEK) { + Elements pages = topic.select("div:contains(Σελίδες:)>a.navPages"); + + if (pages.size() != 0) { + returnPages = currentPage; + for (Element item : pages) { + if (Integer.parseInt(item.text()) > returnPages) + returnPages = Integer.parseInt(item.text()); + } + } + } else { + Elements pages = topic.select("div:contains(Pages:)>a.navPages"); + + if (pages.size() != 0) { + returnPages = currentPage; + for (Element item : pages) { + if (Integer.parseInt(item.text()) > returnPages) + returnPages = Integer.parseInt(item.text()); + } + } + } + + return returnPages; + } + + /** + * Returns current pages's page index. * - * @param topicURL a topic's page URL - * @return the base URL of the given topic + * @param topic {@link Document} object containing this page's source code + * @param language a {@link ParseHelpers.Language} containing this page's + * language set, this is returned by + * {@link ParseHelpers.Language#getLanguage(Document)} + * @return int containing parsed topic's current page + * @see org.jsoup.Jsoup Jsoup */ - public static String getBaseURL(String topicURL) { - String forumUrl = "https://www.thmmy.gr/smf/index.php?"; - Matcher baseUrlMatcher = Pattern.compile("topic=[0-9]+").matcher(topicURL); - if (baseUrlMatcher.find()) - return forumUrl + topicURL.substring(baseUrlMatcher.start(), baseUrlMatcher.end()); - else return ""; + public static int parseCurrentPageIndex(Document topic, ParseHelpers.Language language) { + int parsedPage = 1; + + if (language == ParseHelpers.Language.GREEK) { + Elements findCurrentPage = topic.select("td:contains(Σελίδες:)>b"); + + for (Element item : findCurrentPage) { + if (!item.text().contains("...") + && !item.text().contains("Σελίδες:")) { + parsedPage = Integer.parseInt(item.text()); + break; + } + } + } else { + Elements findCurrentPage = topic.select("td:contains(Pages:)>b"); + + for (Element item : findCurrentPage) { + if (!item.text().contains("...") && !item.text().contains("Pages:")) { + parsedPage = Integer.parseInt(item.text()); + break; + } + } + } + + return parsedPage; + } + + public static int parseCurrentPageIndexInbox(Document topic, ParseHelpers.Language language) { + int parsedPage = 1; + + if (language == ParseHelpers.Language.GREEK) { + Elements findCurrentPage = topic.select("div:contains(Σελίδες:)>b"); + + for (Element item : findCurrentPage) { + if (!item.text().contains("...") + && !item.text().contains("Σελίδες:")) { + parsedPage = Integer.parseInt(item.text()); + break; + } + } + } else { + Elements findCurrentPage = topic.select("div:contains(Pages:)>b"); + + for (Element item : findCurrentPage) { + if (!item.text().contains("...") && !item.text().contains("Pages:")) { + parsedPage = Integer.parseInt(item.text()); + break; + } + } + } + + return parsedPage; } /** diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/StringUtils.java b/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/StringUtils.java new file mode 100644 index 00000000..de13d2d7 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/parsing/StringUtils.java @@ -0,0 +1,30 @@ +package gr.thmmy.mthmmy.utils.parsing; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class StringUtils { + /** + * Method that extracts the base URL from a topic's page 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" + * + * @param topicURL a topic's page URL + * @return the base URL of the given topic + */ + public static String getBaseURL(String topicURL) { + String forumUrl = "https://www.thmmy.gr/smf/index.php?"; + Matcher baseUrlMatcher = Pattern.compile("topic=[0-9]+").matcher(topicURL); + if (baseUrlMatcher.find()) + return forumUrl + topicURL.substring(baseUrlMatcher.start(), baseUrlMatcher.end()); + else return ""; + } + + public static int extractUserCodeFromUrl(String url) { + Matcher userCodeMatcher = Pattern.compile("u=[0-9]+").matcher(url); + if (userCodeMatcher.find()) + return Integer.parseInt(url.substring(userCodeMatcher.start()+2, userCodeMatcher.end())); + else return -1; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/viewmodel/InboxViewModel.java b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/InboxViewModel.java new file mode 100644 index 00000000..bf44098a --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/InboxViewModel.java @@ -0,0 +1,116 @@ +package gr.thmmy.mthmmy.viewmodel; + +import android.os.AsyncTask; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.util.ArrayList; + +import gr.thmmy.mthmmy.activities.inbox.tasks.InboxTask; +import gr.thmmy.mthmmy.model.Inbox; +import gr.thmmy.mthmmy.model.PM; +import gr.thmmy.mthmmy.pagination.BottomPaginationView; +import gr.thmmy.mthmmy.utils.ExternalAsyncTask; +import gr.thmmy.mthmmy.utils.NetworkResultCodes; +import timber.log.Timber; + +public class InboxViewModel extends ViewModel implements InboxTask.OnNetworkTaskFinishedListener, + BottomPaginationView.OnPageRequestedListener { + private static final String INBOX_URL = "https://www.thmmy.gr/smf/index.php?action=pm"; + /** + * caches the expand/collapse state of the user extra info in the current page for the recyclerview + */ + private ArrayList userExtraInfoVisibile = new ArrayList<>(); + + private MutableLiveData pageIndicatorIndex = new MutableLiveData<>(); + private MutableLiveData pageCount = new MutableLiveData<>(); + + private InboxTask currentInboxTask; + + private Inbox inbox; + private InboxTask.OnNetworkTaskFinishedListener onInboxTaskFinishedListener; + private InboxTask.OnTaskStartedListener onInboxTaskStartedListener; + private InboxTask.OnTaskCancelledListener onInboxTaskCancelledListener; + + public void loadInbox() { + loadUrl(INBOX_URL); + } + + public void loadInboxPage(int index) { + loadUrl(INBOX_URL + ";f=inbox;sort=date;start=" + 15*(index-1)); + } + + public void loadUrl(String url) { + stopLoading(); + currentInboxTask = new InboxTask(); + currentInboxTask.setOnTaskStartedListener(onInboxTaskStartedListener); + currentInboxTask.setOnNetworkTaskFinishedListener(this); + currentInboxTask.setOnTaskCancelledListener(onInboxTaskCancelledListener); + currentInboxTask.execute(url); + } + + public void stopLoading() { + if (currentInboxTask != null && currentInboxTask.getStatus() == AsyncTask.Status.RUNNING) { + Timber.i("Canceling inbox task"); + currentInboxTask.cancel(true); + onInboxTaskCancelledListener.onTaskCanceled(); + } + } + + public void setOnInboxTaskFinishedListener(InboxTask.OnNetworkTaskFinishedListener onInboxTaskFinishedListener) { + this.onInboxTaskFinishedListener = onInboxTaskFinishedListener; + } + + public void setOnInboxTaskStartedListener(InboxTask.OnTaskStartedListener onInboxTaskStartedListener) { + this.onInboxTaskStartedListener = onInboxTaskStartedListener; + } + + @Override + public void onPageRequested(int index) { + pageIndicatorIndex.setValue(index); + loadInboxPage(index); + } + + @Override + public void onNetworkTaskFinished(int resultCode, Inbox inbox) { + this.inbox = inbox; + onInboxTaskFinishedListener.onNetworkTaskFinished(resultCode, inbox); + if (resultCode == NetworkResultCodes.SUCCESSFUL) { + userExtraInfoVisibile.clear(); + for (PM pm : inbox.getPms()) + userExtraInfoVisibile.add(false); + + pageIndicatorIndex.setValue(inbox.getCurrentPageIndex()); + pageCount.setValue(inbox.getNumberOfPages()); + } + } + + public void setOnInboxTaskCancelledListener(InboxTask.OnTaskCancelledListener onInboxTaskCancelledListener) { + this.onInboxTaskCancelledListener = onInboxTaskCancelledListener; + } + + public Inbox getInbox() { + return inbox; + } + + public MutableLiveData getPageCount() { + return pageCount; + } + + public MutableLiveData getPageIndicatorIndex() { + return pageIndicatorIndex; + } + + public boolean isUserExtraInfoVisible(int position) { + return userExtraInfoVisibile.get(position); + } + + public void hideUserInfo(int position) { + userExtraInfoVisibile.set(position, false); + } + + public void toggleUserInfo(int position) { + userExtraInfoVisibile.set(position, !userExtraInfoVisibile.get(position)); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java index bac3952a..f1b43462 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java +++ b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java @@ -28,14 +28,16 @@ import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.model.Poll; import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.TopicItem; +import gr.thmmy.mthmmy.pagination.BottomPaginationView; import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.utils.ExternalAsyncTask; import gr.thmmy.mthmmy.utils.NetworkTask; -import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import gr.thmmy.mthmmy.utils.parsing.StringUtils; import timber.log.Timber; public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTaskCompleted, - PrepareForReplyTask.OnPrepareForReplyFinished, PrepareForEditTask.OnPrepareEditFinished { + PrepareForReplyTask.OnPrepareForReplyFinished, PrepareForEditTask.OnPrepareEditFinished, + BottomPaginationView.OnPageRequestedListener { /** * topic state */ @@ -76,6 +78,7 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa * navigation bar occurs, aka the value that the page indicator shows */ private MutableLiveData pageIndicatorIndex = new MutableLiveData<>(); + private MutableLiveData pageCount = new MutableLiveData<>(); private MutableLiveData replyPageUrl = new MutableLiveData<>(); private MutableLiveData pageTopicId = new MutableLiveData<>(); @@ -87,7 +90,6 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa private MutableLiveData topicViewers = new MutableLiveData<>(); private String topicUrl; private int currentPageIndex; - private int pageCount; private MutableLiveData prepareForReplyResult = new MutableLiveData<>(); private MutableLiveData prepareForEditResult = new MutableLiveData<>(); @@ -112,7 +114,7 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa public void resetPage() { if (topicUrl == null) throw new NullPointerException("No topic task has been requested yet!"); Timber.i("Resetting page"); - loadUrl(ParseHelpers.getBaseURL(topicUrl) + "." + String.valueOf(currentPageIndex * 15)); + loadUrl(StringUtils.getBaseURL(topicUrl) + "." + currentPageIndex * 15); } public void resetPageThen(Runnable runnable) { @@ -123,16 +125,22 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa TopicViewModel.this.onTopicTaskCompleted(result); runnable.run(); }); - currentTopicTask.execute(ParseHelpers.getBaseURL(topicUrl) + "." + String.valueOf(currentPageIndex * 15)); + currentTopicTask.execute(StringUtils.getBaseURL(topicUrl) + "." + currentPageIndex * 15); } - public void loadPageIndicated() { + @Override + public void onPageRequested(int index) { + pageIndicatorIndex.setValue(index); + loadPageIndicated(); + } + + private void loadPageIndicated() { if (pageIndicatorIndex.getValue() == null) throw new NullPointerException("No page has been loaded yet!"); int pageRequested = pageIndicatorIndex.getValue() - 1; if (pageRequested != currentPageIndex - 1) { Timber.i("Changing to page " + pageRequested + 1); - loadUrl(ParseHelpers.getBaseURL(topicUrl) + "." + String.valueOf(pageRequested * 15)); + loadUrl(StringUtils.getBaseURL(topicUrl) + "." + pageRequested * 15); pageIndicatorIndex.setValue(pageRequested + 1); } else { stopLoading(); @@ -172,10 +180,14 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa } public void prepareForReply() { - if (replyPageUrl.getValue() == null) + if (replyPageUrl.getValue() == null || pageCount.getValue() == null || pageIndicatorIndex.getValue() ==null) throw new NullPointerException("Topic task has not finished yet!"); stopLoading(); - setPageIndicatorIndex(pageCount, true); + // go to last page for reply + if (currentPageIndex != pageCount.getValue()) { + this.pageIndicatorIndex.setValue(pageCount.getValue()); + loadPageIndicated(); + } Timber.i("Preparing for reply"); currentPrepareForReplyTask = new PrepareForReplyTask(prepareForReplyCallbacks, this, replyPageUrl.getValue()); @@ -252,9 +264,11 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa // callbacks for viewmodel @Override public void onTopicTaskCompleted(TopicTaskResult result) { + //sets Data if (result.getResultCode() == TopicTask.ResultCode.SUCCESS) { currentPageIndex = result.getCurrentPageIndex(); - pageCount = result.getPageCount(); + pageIndicatorIndex.setValue(result.getCurrentPageIndex()); + pageCount.setValue(result.getPageCount()); topicTreeAndMods.setValue(result.getTopicTreeAndMods()); topicViewers.setValue(result.getTopicViewers()); pageTopicId.setValue(result.getLoadedPageTopicId()); @@ -267,6 +281,7 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa for (int i = 0; i < result.getNewPostsList().size(); i++) isUserExtraInfoVisibile.add(false); } + // see also callback in TopicActivity, sets UI topicTaskResultCode.setValue(result.getResultCode()); } @@ -281,36 +296,6 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa prepareForEditResult.setValue(result); } - public void incrementPageRequestValue(int step, boolean changePage) { - if (pageIndicatorIndex.getValue() == null) - throw new NullPointerException("No page has been loaded yet!"); - int oldIndicatorIndex = pageIndicatorIndex.getValue(); - if (oldIndicatorIndex <= pageCount - step) - pageIndicatorIndex.setValue(pageIndicatorIndex.getValue() + step); - else - pageIndicatorIndex.setValue(pageCount); - if (changePage && oldIndicatorIndex != pageIndicatorIndex.getValue()) loadPageIndicated(); - } - - public void decrementPageRequestValue(int step, boolean changePage) { - if (pageIndicatorIndex.getValue() == null) - throw new NullPointerException("No page has been loaded yet!"); - int oldIndicatorIndex = pageIndicatorIndex.getValue(); - if (oldIndicatorIndex > step) - pageIndicatorIndex.setValue(pageIndicatorIndex.getValue() - step); - else - pageIndicatorIndex.setValue(1); - if (changePage && oldIndicatorIndex != pageIndicatorIndex.getValue()) loadPageIndicated(); - } - - public void setPageIndicatorIndex(int pageIndicatorIndex, boolean changePage) { - if (this.pageIndicatorIndex.getValue() == null) - throw new NullPointerException("No page has been loaded yet!"); - int oldIndicatorIndex = this.pageIndicatorIndex.getValue(); - this.pageIndicatorIndex.setValue(pageIndicatorIndex); - if (changePage && oldIndicatorIndex != this.pageIndicatorIndex.getValue()) loadPageIndicated(); - } - // <-------------Just getters, setters and helper methods below here----------------> public int getTopicId() { @@ -465,8 +450,7 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa return currentPageIndex; } - public int getPageCount() { - if (pageCount == 0) throw new NullPointerException("No page has been loaded yet!"); + public MutableLiveData getPageCount() { return pageCount; } diff --git a/app/src/main/res/drawable/ic_message_white_24dp.xml b/app/src/main/res/drawable/ic_message_white_24dp.xml new file mode 100644 index 00000000..a935c9d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_create_pm.xml b/app/src/main/res/layout/activity_create_pm.xml new file mode 100644 index 00000000..f0405463 --- /dev/null +++ b/app/src/main/res/layout/activity_create_pm.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_create_content.xml b/app/src/main/res/layout/activity_create_topic.xml similarity index 66% rename from app/src/main/res/layout/activity_create_content.xml rename to app/src/main/res/layout/activity_create_topic.xml index 7e6a9b40..e7f65e47 100644 --- a/app/src/main/res/layout/activity_create_content.xml +++ b/app/src/main/res/layout/activity_create_topic.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - tools:context=".activities.create_content.CreateContentActivity" + tools:context=".activities.create_topic.CreateTopicActivity" android:layout_height="match_parent" android:layout_width="match_parent" android:fitsSystemWindows="true"> @@ -29,31 +29,11 @@ - - - - - - + android:layout_width="match_parent"/> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_inbox_overflow_menu.xml b/app/src/main/res/layout/activity_inbox_overflow_menu.xml new file mode 100644 index 00000000..0ecaf6ca --- /dev/null +++ b/app/src/main/res/layout/activity_inbox_overflow_menu.xml @@ -0,0 +1,49 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_inbox_pm_row.xml b/app/src/main/res/layout/activity_inbox_pm_row.xml new file mode 100644 index 00000000..c4bdced2 --- /dev/null +++ b/app/src/main/res/layout/activity_inbox_pm_row.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_topic.xml b/app/src/main/res/layout/activity_topic.xml index b61c41cf..7b576ffc 100644 --- a/app/src/main/res/layout/activity_topic.xml +++ b/app/src/main/res/layout/activity_topic.xml @@ -71,63 +71,14 @@ android:visibility="gone" /> - - - - - - - - - - - - + app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareLinearBehavior"/> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pagination.xml b/app/src/main/res/layout/pagination.xml new file mode 100644 index 00000000..fbd2b128 --- /dev/null +++ b/app/src/main/res/layout/pagination.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1593d8ae..2d417ab4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,8 @@ About Home Bookmarks + Shoutbox + Inbox Info OK "Cancel" @@ -21,7 +23,6 @@ Recent Forum Unread - Shoutbox Refresh @@ -232,4 +233,12 @@ New topic Create topic URL copied + + + Personal message author thumbnail + PM author + PM subject + PM content + Quote + Reply diff --git a/app/src/test/java/gr/thmmy/mthmmy/utils/parsing/StringUtilsTest.java b/app/src/test/java/gr/thmmy/mthmmy/utils/parsing/StringUtilsTest.java new file mode 100644 index 00000000..fd41a65d --- /dev/null +++ b/app/src/test/java/gr/thmmy/mthmmy/utils/parsing/StringUtilsTest.java @@ -0,0 +1,19 @@ +package gr.thmmy.mthmmy.utils.parsing; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class StringUtilsTest { + + @Before + public void setUp() throws Exception { + } + + @Test + public void extractUserCodeFromUrl() { + String url = "https://www.thmmy.gr/smf/index.php?action=profile;u=14670"; + assertEquals(StringUtils.extractUserCodeFromUrl(url), 14670); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 655542d9..316b8d1a 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { maven { url "https://jitpack.io" } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'com.android.tools.build:gradle:3.6.1' classpath 'com.google.gms:google-services:4.3.2' classpath 'io.fabric.tools:gradle:1.29.0' classpath 'org.ajoberstar.grgit:grgit-core:3.1.1' // Also change in app/gradle/grgit.gradle diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7076a01e..dd0ac2c6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Sep 17 12:32:34 EEST 2019 +#Mon Mar 30 15:17:37 EEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
Class contains the methods: