Browse Source

Merge 17f94657d8 into 07adaa9e75

pull/67/merge
oogee 5 years ago
committed by GitHub
parent
commit
a94c15b753
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      app/build.gradle
  2. 13
      app/src/main/AndroidManifest.xml
  3. 6
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java
  4. 126
      app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/CreatePMActivity.java
  5. 88
      app/src/main/java/gr/thmmy/mthmmy/activities/create_pm/SendPMTask.java
  6. 6
      app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/CreateTopicActivity.java
  7. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/create_topic/NewTopicTask.java
  8. 94
      app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxActivity.java
  9. 374
      app/src/main/java/gr/thmmy/mthmmy/activities/inbox/InboxAdapter.java
  10. 203
      app/src/main/java/gr/thmmy/mthmmy/activities/inbox/tasks/InboxTask.java
  11. 39
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java
  12. 237
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  13. 20
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java
  14. 125
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java
  15. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java
  16. 26
      app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java
  17. 32
      app/src/main/java/gr/thmmy/mthmmy/model/Inbox.java
  18. 157
      app/src/main/java/gr/thmmy/mthmmy/model/PM.java
  19. 263
      app/src/main/java/gr/thmmy/mthmmy/pagination/BottomPaginationView.java
  20. 6
      app/src/main/java/gr/thmmy/mthmmy/utils/MessageAnimations.java
  21. 177
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java
  22. 30
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/StringUtils.java
  23. 116
      app/src/main/java/gr/thmmy/mthmmy/viewmodel/InboxViewModel.java
  24. 68
      app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java
  25. 5
      app/src/main/res/drawable/ic_message_white_24dp.xml
  26. 56
      app/src/main/res/layout/activity_create_pm.xml
  27. 28
      app/src/main/res/layout/activity_create_topic.xml
  28. 71
      app/src/main/res/layout/activity_inbox.xml
  29. 49
      app/src/main/res/layout/activity_inbox_overflow_menu.xml
  30. 212
      app/src/main/res/layout/activity_inbox_pm_row.xml
  31. 55
      app/src/main/res/layout/activity_topic.xml
  32. 32
      app/src/main/res/layout/full_post_editor.xml
  33. 54
      app/src/main/res/layout/pagination.xml
  34. 11
      app/src/main/res/values/strings.xml
  35. 19
      app/src/test/java/gr/thmmy/mthmmy/utils/parsing/StringUtilsTest.java
  36. 2
      build.gradle
  37. 4
      gradle/wrapper/gradle-wrapper.properties

8
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'

13
app/src/main/AndroidManifest.xml

@ -18,6 +18,7 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
@ -171,7 +172,7 @@
</receiver>
<activity
android:name=".activities.create_content.CreateContentActivity"
android:name=".activities.create_topic.CreateTopicActivity"
android:configChanges="orientation|screenSize"
android:parentActivityName=".activities.main.MainActivity"
android:theme="@style/AppTheme.NoActionBar" />
@ -183,6 +184,16 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.main.MainActivity" />
</activity>
<activity
android:name=".activities.create_pm.CreatePMActivity"
android:configChanges="orientation|screenSize"
android:theme="@style/AppTheme.NoActionBar" />
<activity android:name=".activities.inbox.InboxActivity"
android:theme="@style/AppTheme.NoActionBar" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.main.MainActivity" />
</activity>
</application>
</manifest>

6
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 {

126
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<Boolean> {
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();
}
}
}

88
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<String, Boolean> {
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;
}
}
}

6
app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java → 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);

2
app/src/main/java/gr/thmmy/mthmmy/activities/create_content/NewTopicTask.java → 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;

94
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);
});
}
}

374
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<RecyclerView.ViewHolder> {
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<PM> 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;
}
}
}

203
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<Inbox> {
@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<PM> pmList = parsePMs(document, language);
inbox.setPms(pmList);
return inbox;
}
@Override
protected int getResultCode(Response response, Inbox data) {
return NetworkResultCodes.SUCCESSFUL;
}
private ArrayList<PM> parsePMs(Document document, ParseHelpers.Language language) {
ArrayList<PM> 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 = "<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />" + 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<String> infoList = Arrays.asList(authorInfoContainer.html().split("<br>"));
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("<a href=")) {
String personalText = thisLine;
personalText = personalText.replace("\n", "").replace("\r", "").trim();
pm.setAuthorPersonalText(personalText);
}
}
//Checks post for mentions of this user (if the user is logged in)
if (BaseActivity.getSessionManager().isLoggedIn() &&
ParseHelpers.mentionsPattern.matcher(pm.getContent()).find()) {
pm.setUserMentioned(true);
}
}
pms.add(pm);
}
return pms;
}
}

39
app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java

@ -18,6 +18,7 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.res.ResourcesCompat;
import androidx.fragment.app.Fragment;
@ -39,6 +40,8 @@ import java.util.List;
import java.util.Objects;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.LoginActivity;
import gr.thmmy.mthmmy.activities.create_pm.CreatePMActivity;
import gr.thmmy.mthmmy.activities.profile.latestPosts.LatestPostsFragment;
import gr.thmmy.mthmmy.activities.profile.stats.StatsFragment;
import gr.thmmy.mthmmy.activities.profile.summary.SummaryFragment;
@ -94,6 +97,7 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
private String profileUrl;
private String avatarUrl;
private String username;
private String sendPmUrl;
private int tabSelect;
//Fix for vector drawables on android <21
@ -142,37 +146,29 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
viewPager = findViewById(R.id.profile_tab_container);
pmFAB = findViewById(R.id.profile_fab);
pmFAB.setEnabled(false);
pmFAB.hide();
/*if (!sessionManager.isLoggedIn()) pmFAB.hide();
if (!sessionManager.isLoggedIn()) pmFAB.hide();
else {
pmFAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
pmFAB.setOnClickListener(view -> {
if (sessionManager.isLoggedIn()) {
//TODO PM
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 sent a personal message!")
.setPositiveButton("Login", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
.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", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
.setNegativeButton("Cancel", (dialogInterface, i) -> {
})
.show();
}
}
});
}*/
}
ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(Uri.parse(profileUrl));
if (!target.is(ThmmyPage.PageCategory.PROFILE)) {
@ -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;
}

237
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

20
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<RecyclerView.ViewHolder> {
} 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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
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;

125
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.
* <p>Class contains the methods:<ul><li>{@link #parseUsersViewingThisTopic(Document,
* ParseHelpers.Language)}</li>
* <li>{@link #parseCurrentPageIndex(Document, ParseHelpers.Language)}</li>
* <li>{@link #parseTopicNumberOfPages(Document, int, ParseHelpers.Language)}</li>
* <li>{@link #parseTopic(Document, ParseHelpers.Language)}</li>
*/
public class TopicParser {
private static Pattern mentionsPattern = Pattern.
compile("<div class=\"quoteheader\">\\n\\s+?<a href=.+?>(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;
}
}

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

@ -97,10 +97,10 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
, 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<TopicItem> newPostsList = TopicParser.parseTopic(topic, language);

26
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);

32
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<PM> 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<PM> getPms() {
return pms;
}
public void setPms(ArrayList<PM> pms) {
this.pms = pms;
}
}

157
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;
}
}

263
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);
}
}

6
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAnimations.java → 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

177
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("<div class=\"quoteheader\">\\n\\s+?<a href=.+?>(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:<ul>
* <li>{@link #PAGE_INCOMPLETE}</li>
@ -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;
}
/**

30
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;
}
}

116
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<Inbox>,
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<Boolean> userExtraInfoVisibile = new ArrayList<>();
private MutableLiveData<Integer> pageIndicatorIndex = new MutableLiveData<>();
private MutableLiveData<Integer> pageCount = new MutableLiveData<>();
private InboxTask currentInboxTask;
private Inbox inbox;
private InboxTask.OnNetworkTaskFinishedListener<Inbox> 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<Inbox> 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<Integer> getPageCount() {
return pageCount;
}
public MutableLiveData<Integer> 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));
}
}

68
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<Integer> pageIndicatorIndex = new MutableLiveData<>();
private MutableLiveData<Integer> pageCount = new MutableLiveData<>();
private MutableLiveData<String> replyPageUrl = new MutableLiveData<>();
private MutableLiveData<Integer> pageTopicId = new MutableLiveData<>();
@ -87,7 +90,6 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa
private MutableLiveData<String> topicViewers = new MutableLiveData<>();
private String topicUrl;
private int currentPageIndex;
private int pageCount;
private MutableLiveData<PrepareForReplyResult> prepareForReplyResult = new MutableLiveData<>();
private MutableLiveData<PrepareForEditResult> 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<Integer> getPageCount() {
return pageCount;
}

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

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</vector>

56
app/src/main/res/layout/activity_create_pm.xml

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activities.create_pm.CreatePMActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ToolbarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:gravity="center"
app:popupTheme="@style/ToolbarTheme"></androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<include
layout="@layout/full_post_editor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/appbar" />
<gr.thmmy.mthmmy.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone" />
</RelativeLayout>
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progressBar"
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
android:layout_width="match_parent"
android:layout_height="@dimen/progress_bar_height"
android:indeterminate="true"
android:visibility="invisible"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|center"
app:mpb_indeterminateTint="@color/accent"
app:mpb_progressStyle="horizontal" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

28
app/src/main/res/layout/activity_create_content.xml → 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 @@
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/subject_input"
android:layout_width="240dp"
android:layout_height="wrap_content"
<include
layout="@layout/full_post_editor"
android:layout_below="@id/appbar"
android:layout_margin="16dp"
android:hint="@string/subject">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:maxLines="1"
android:ellipsize="end"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<gr.thmmy.mthmmy.editorview.EditorView
android:id="@+id/main_content_editorview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/subject_input"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:hint="topic message"/>
android:layout_width="match_parent"/>
<gr.thmmy.mthmmy.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard"

71
app/src/main/res/layout/activity_inbox.xml

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activities.inbox.InboxActivity">
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
android:layout_width="match_parent"
android:layout_height="@dimen/progress_bar_height"
android:indeterminate="true"
android:visibility="invisible"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|center"
app:mpb_indeterminateTint="@color/accent"
app:mpb_progressStyle="horizontal" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ToolbarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:gravity="center"
app:popupTheme="@style/ToolbarTheme" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/inbox_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/appbar"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="gr.thmmy.mthmmy.activities.topic.TopicActivity"
tools:listitem="@layout/activity_inbox_pm_row" />
</RelativeLayout>
<gr.thmmy.mthmmy.pagination.BottomPaginationView
android:id="@+id/bottom_pagination"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="bottom|end"
android:background="@color/primary"
app:elevation="8dp"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareLinearBehavior"/>
<!--
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/send_pm_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="50dp"
android:layout_marginEnd="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_message_white_24dp" />
-->
</androidx.coordinatorlayout.widget.CoordinatorLayout>

49
app/src/main/res/layout/activity_inbox_overflow_menu.xml

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/pm_quote_button"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:background="?android:attr/selectableItemBackground"
android:drawablePadding="5dp"
android:gravity="center_vertical"
android:paddingBottom="6dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:text="@string/quote_menu_button_text"
android:textColor="@color/primary_text" />
<TextView
android:id="@+id/pm_reply_button"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:background="?android:attr/selectableItemBackground"
android:drawablePadding="5dp"
android:gravity="center_vertical"
android:paddingBottom="6dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:text="@string/reply_menu_button_text"
android:textColor="@color/primary_text" />
<TextView
android:id="@+id/delete_post"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:background="?android:attr/selectableItemBackground"
android:drawablePadding="5dp"
android:gravity="center_vertical"
android:paddingBottom="6dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:text="@string/post_delete_button"
android:textColor="@color/primary_text" />
</LinearLayout>

212
app/src/main/res/layout/activity_inbox_pm_row.xml

@ -0,0 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingEnd="4dp"
android:paddingStart="4dp"
tools:ignore="SmallSp">
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardBackgroundColor="@color/card_background"
card_view:cardCornerRadius="5dp"
card_view:cardElevation="2dp"
card_view:cardPreventCornerOverlap="false"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:id="@+id/card_child_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp">
<FrameLayout
android:id="@+id/thumbnail_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="@dimen/thumbnail_size"
android:layout_height="@dimen/thumbnail_size"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/personal_message_thumbnail"
android:transitionName="user_thumbnail"
app:srcCompat="@drawable/ic_default_user_avatar_darker" />
</FrameLayout>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/thumbnail_holder"
android:ellipsize="end"
android:maxLines="1"
tools:text="@string/pm_author"
android:textColor="@color/primary_text"
android:textStyle="bold" />
<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/username"
android:layout_toEndOf="@+id/thumbnail_holder"
android:ellipsize="end"
android:maxLines="1"
tools:text="@string/pm_subject" />
</RelativeLayout>
<ImageButton
android:id="@+id/pm_overflow_menu"
android:layout_width="@dimen/post_image_button"
android:layout_height="@dimen/post_image_button"
android:layout_marginTop="9dp"
android:layout_marginEnd="9dp"
android:background="@color/card_background"
android:clickable="true"
android:contentDescription="@string/post_overflow_menu_button"
android:focusable="true"
app:srcCompat="@drawable/ic_more_vert_white_24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/user_extra_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="3dp"
android:visibility="gone"
android:weightSum="1.0">
<TextView
android:id="@+id/special_rank"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/rank"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/stars"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/number_of_posts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/personal_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:textStyle="italic"
android:visibility="gone" />
</LinearLayout>
<FrameLayout
android:id="@+id/pm_date_exp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/pm_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:text=""
android:textColor="@color/accent"
android:textSize="11sp" />
</FrameLayout>
<View
android:id="@+id/header_body_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="9dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="5dp"
android:background="@color/divider" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp">
<WebView
android:id="@+id/pm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:background="@color/card_background"
android:clickable="true"
android:focusable="true"
tools:text="@string/pm_content" />
</FrameLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

55
app/src/main/res/layout/activity_topic.xml

@ -71,63 +71,14 @@
android:visibility="gone" />
</RelativeLayout>
<LinearLayout
android:id="@+id/bottom_navigation_bar"
<gr.thmmy.mthmmy.pagination.BottomPaginationView
android:id="@+id/bottom_pagination"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="bottom|end"
android:background="@color/primary"
app:elevation="8dp"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareLinearBehavior">
<ImageButton
android:id="@+id/page_first_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_first"
app:srcCompat="@drawable/page_first" />
<ImageButton
android:id="@+id/page_previous_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_previous"
app:srcCompat="@drawable/page_previous" />
<TextView
android:id="@+id/page_indicator"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:hint="@string/button_page"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="22sp" />
<ImageButton
android:id="@+id/page_next_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_next"
app:srcCompat="@drawable/page_next" />
<ImageButton
android:id="@+id/page_last_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_last"
app:srcCompat="@drawable/page_last" />
</LinearLayout>
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareLinearBehavior"/>
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progressBar"

32
app/src/main/res/layout/full_post_editor.xml

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/subject_input"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:hint="@string/subject">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:maxLines="1"
android:ellipsize="end"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
<gr.thmmy.mthmmy.editorview.EditorView
android:id="@+id/main_content_editorview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:hint="topic message"/>
</LinearLayout>

54
app/src/main/res/layout/pagination.xml

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/primary"
app:elevation="8dp"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareLinearBehavior">
<ImageButton
android:id="@+id/page_first_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_first"
app:srcCompat="@drawable/page_first" />
<ImageButton
android:id="@+id/page_previous_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_previous"
app:srcCompat="@drawable/page_previous" />
<TextView
android:id="@+id/page_indicator"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:hint="@string/button_page"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="22sp" />
<ImageButton
android:id="@+id/page_next_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_next"
app:srcCompat="@drawable/page_next" />
<ImageButton
android:id="@+id/page_last_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/button_last"
app:srcCompat="@drawable/page_last" />
</merge>

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

@ -11,6 +11,8 @@
<string name="about">About</string>
<string name="home">Home</string>
<string name="bookmark">Bookmarks</string>
<string name="shoutbox">Shoutbox</string>
<string name="inbox">Inbox</string>
<string name="info">Info</string>
<string name="ok">OK</string>
<string name="cancel">"Cancel"</string>
@ -21,7 +23,6 @@
<string name="recent">Recent</string>
<string name="forum">Forum</string>
<string name="unread">Unread</string>
<string name="shoutbox">Shoutbox</string>
<string name="refresh">Refresh</string>
<!--Login Activity-->
@ -232,4 +233,12 @@
<string name="new_topic_toolbar">New topic</string>
<string name="create_topic">Create topic</string>
<string name="url_copied_msg">URL copied</string>
<!-- Inbox activity -->
<string name="personal_message_thumbnail">Personal message author thumbnail</string>
<string name="pm_author">PM author</string>
<string name="pm_subject">PM subject</string>
<string name="pm_content">PM content</string>
<string name="quote_menu_button_text">Quote</string>
<string name="reply_menu_button_text">Reply</string>
</resources>

19
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);
}
}

2
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

4
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

Loading…
Cancel
Save