Browse Source

Version 1.8.0

master v1.8.0
Ezerous 5 years ago
parent
commit
251f6c5fa9
No known key found for this signature in database GPG Key ID: 262B2954BBA319E3
  1. 2
      README.md
  2. 39
      app/build.gradle
  3. 9
      app/proguard-rules.pro
  4. 7
      app/src/main/AndroidManifest.xml
  5. 60
      app/src/main/assets/apache_libraries.html
  6. 229
      app/src/main/assets/epl_libraries.html
  7. 28
      app/src/main/assets/libraries_style.css
  8. 42
      app/src/main/assets/mit_libraries.html
  9. 114
      app/src/main/assets/other_libraries.html
  10. 8
      app/src/main/assets/style.css
  11. BIN
      app/src/main/ic_launcher-playstore.png
  12. 51
      app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java
  13. 30
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java
  14. 8
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java
  15. 40
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksActivity.java
  16. 114
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java
  17. 158
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksTopicFragment.java
  18. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java
  19. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java
  20. 17
      app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java
  21. 15
      app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java
  22. 38
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java
  23. 28
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java
  24. 101
      app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadAdapter.java
  25. 278
      app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadFragment.java
  26. 54
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java
  27. 11
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java
  28. 17
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java
  29. 3
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java
  30. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java
  31. 1
      app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsActivity.java
  32. 21
      app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsFragment.java
  33. 5
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutAdapter.java
  34. 12
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxFragment.java
  35. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxTask.java
  36. 63
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  37. 38
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java
  38. 11
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java
  39. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForEditTask.java
  40. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/PrepareForReplyTask.java
  41. 27
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java
  42. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadActivity.java
  43. 6
      app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadFieldsBuilderActivity.java
  44. 69
      app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java
  45. 50
      app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java
  46. 21
      app/src/main/java/gr/thmmy/mthmmy/model/ThmmyFile.java
  47. 39
      app/src/main/java/gr/thmmy/mthmmy/model/Topic.java
  48. 55
      app/src/main/java/gr/thmmy/mthmmy/model/TopicSummary.java
  49. 5
      app/src/main/java/gr/thmmy/mthmmy/services/DownloadHelper.java
  50. 113
      app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java
  51. 18
      app/src/main/java/gr/thmmy/mthmmy/session/ValidateSessionTask.java
  52. 47
      app/src/main/java/gr/thmmy/mthmmy/utils/CircleTransform.java
  53. 73
      app/src/main/java/gr/thmmy/mthmmy/utils/DateTimeUtils.java
  54. 9
      app/src/main/java/gr/thmmy/mthmmy/utils/NetworkTask.java
  55. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReporter.java
  56. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReportingTree.java
  57. 39
      app/src/main/java/gr/thmmy/mthmmy/utils/io/AssetUtils.java
  58. 54
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java
  59. 5
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseTask.java
  60. 112
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParser.java
  61. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/CenterVerticalSpan.java
  62. 65
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/ImageDownloadDialogBuilder.java
  63. 63
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/PhotoViewUtils.java
  64. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareFABBehavior.java
  65. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareLinearBehavior.java
  66. 2
      app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java
  67. 2
      app/src/main/java/gr/thmmy/mthmmy/views/AppCompatSpinnerWithoutDefault.java
  68. 2
      app/src/main/java/gr/thmmy/mthmmy/views/CustomLinearLayoutManager.java
  69. 2
      app/src/main/java/gr/thmmy/mthmmy/views/CustomRecyclerView.java
  70. 93
      app/src/main/java/gr/thmmy/mthmmy/views/ReactiveWebView.java
  71. 232
      app/src/main/java/gr/thmmy/mthmmy/views/RelativeTimeTextView.java
  72. 2
      app/src/main/java/gr/thmmy/mthmmy/views/ToggledBackgroundButton.java
  73. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EditorView.java
  74. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiInputField.java
  75. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboard.java
  76. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboardAdapter.java
  77. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/FormatButtonsAdapter.java
  78. 2
      app/src/main/java/gr/thmmy/mthmmy/views/editorview/IEmojiKeyboard.java
  79. 5
      app/src/main/res/drawable/ic_file_not_found.xml
  80. 26
      app/src/main/res/drawable/ic_launcher_foreground.xml
  81. 5
      app/src/main/res/drawable/ic_mark_as_read.xml
  82. 9
      app/src/main/res/layout-v21/activity_profile.xml
  83. 2
      app/src/main/res/layout-v21/activity_topic_post_row.xml
  84. 30
      app/src/main/res/layout/activity_about.xml
  85. 2
      app/src/main/res/layout/activity_board.xml
  86. 4
      app/src/main/res/layout/activity_create_content.xml
  87. 2
      app/src/main/res/layout/activity_downloads.xml
  88. 9
      app/src/main/res/layout/activity_profile.xml
  89. 6
      app/src/main/res/layout/activity_topic.xml
  90. 2
      app/src/main/res/layout/activity_topic_edit_row.xml
  91. 2
      app/src/main/res/layout/activity_topic_post_row.xml
  92. 2
      app/src/main/res/layout/activity_topic_quick_reply_row.xml
  93. 6
      app/src/main/res/layout/activity_upload.xml
  94. 15
      app/src/main/res/layout/fragment_bookmarks.xml
  95. 2
      app/src/main/res/layout/fragment_forum.xml
  96. 2
      app/src/main/res/layout/fragment_latest_posts_row.xml
  97. 2
      app/src/main/res/layout/fragment_recent.xml
  98. 2
      app/src/main/res/layout/fragment_recent_row.xml
  99. 7
      app/src/main/res/layout/fragment_shoutbox.xml
  100. 2
      app/src/main/res/layout/fragment_shoutbox_shout_row.xml

2
README.md

@ -4,7 +4,7 @@
[![Discord Channel](https://img.shields.io/badge/discord-public@mTHMMY-738bd7.svg?style=flat)][discord-server] [![Discord Channel](https://img.shields.io/badge/discord-public@mTHMMY-738bd7.svg?style=flat)][discord-server]
![Last Commit](https://img.shields.io/github/last-commit/ThmmyNoLife/mTHMMY/develop.svg?style=flat) ![Last Commit](https://img.shields.io/github/last-commit/ThmmyNoLife/mTHMMY/develop.svg?style=flat)
![mTHMMY logo](app/src/main/res/mipmap-xhdpi/ic_launcher.png) ![mTHMMY logo](app/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
A mobile app for [thmmy.gr](https://www.thmmy.gr). A mobile app for [thmmy.gr](https://www.thmmy.gr).

39
app/build.gradle

@ -7,15 +7,15 @@ apply plugin: 'io.fabric'
android { android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion = '29.0.2' buildToolsVersion = '29.0.3'
defaultConfig { defaultConfig {
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
applicationId "gr.thmmy.mthmmy" applicationId "gr.thmmy.mthmmy"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
versionCode 22 versionCode 23
versionName "1.7.4" versionName "1.8.0"
archivesBaseName = "mTHMMY-v$versionName" archivesBaseName = "mTHMMY-v$versionName"
buildConfigField "String", "CURRENT_BRANCH", "\"" + getCurrentBranch() + "\"" buildConfigField "String", "CURRENT_BRANCH", "\"" + getCurrentBranch() + "\""
buildConfigField "String", "COMMIT_HASH", "\"" + getCommitHash() + "\"" buildConfigField "String", "COMMIT_HASH", "\"" + getCommitHash() + "\""
@ -29,6 +29,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug { debug {
multiDexEnabled true
def date = new Date().format('ddMMyy_HHmmss') def date = new Date().format('ddMMyy_HHmmss')
archivesBaseName = archivesBaseName + "-$date" archivesBaseName = archivesBaseName + "-$date"
// Disable fabric build ID generation for debug builds // Disable fabric build ID generation for debug builds
@ -72,37 +73,45 @@ tasks.whenTaskAdded { task ->
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":emojis")
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha04' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.exifinterface:exifinterface:1.1.0-beta01' implementation 'androidx.exifinterface:exifinterface:1.2.0'
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.firebase:firebase-core:17.0.0' implementation 'com.google.firebase:firebase-analytics:17.4.2'
implementation 'com.google.firebase:firebase-messaging:19.0.1' implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.snatik:storage:2.1.0' implementation 'com.snatik:storage:2.1.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.0' //TODO: Warning: okhttp has dropped support for Android v.19 since okhttp 3.13! implementation 'com.squareup.okhttp3:okhttp:3.12.12' //TODO: Warning: okhttp has dropped support for Android v.19 since okhttp 3.13!
implementation 'com.squareup.picasso:picasso:2.5.2'
implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
implementation 'org.jsoup:jsoup:1.10.3' //TODO: Warning: upgrading from 1.10.3 will break stuff! implementation 'org.jsoup:jsoup:1.10.3' //TODO: Warning: upgrading from 1.10.3 will break stuff!
implementation 'joda-time:joda-time:2.10.4'
implementation 'com.github.franmontiel:PersistentCookieJar:1.0.1' implementation 'com.github.franmontiel:PersistentCookieJar:1.0.1'
implementation 'com.github.PhilJay:MPAndroidChart:3.0.3' implementation 'com.github.PhilJay:MPAndroidChart:3.0.3'
implementation 'com.mikepenz:materialdrawer:6.1.1' implementation 'com.mikepenz:materialdrawer:6.1.1'
implementation 'com.mikepenz:fontawesome-typeface:4.7.0.0@aar' implementation 'com.mikepenz:fontawesome-typeface:4.7.0.0@aar'
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar' implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.19'
implementation 'com.bignerdranch.android:expandablerecyclerview:3.0.0-RC1'//TODO: deprecated! implementation 'com.bignerdranch.android:expandablerecyclerview:3.0.0-RC1'//TODO: deprecated!
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'me.zhanghai.android.materialprogressbar:library:1.4.2' implementation 'me.zhanghai.android.materialprogressbar:library:1.4.2'
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'ru.noties:markwon:2.0.2' implementation 'ru.noties:markwon:2.0.2'
implementation 'net.gotev:uploadservice:3.5.2' 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 '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 implementation 'com.itkacher.okhttpprofiler:okhttpprofiler:1.0.7' //Plugin: https://plugins.jetbrains.com/plugin/11249-okhttp-profiler
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.powermock:powermock-core:2.0.2'
testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

9
app/proguard-rules.pro

@ -29,8 +29,13 @@
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.* -dontwarn org.codehaus.mojo.animal_sniffer.*
# Picasso #Glide
-dontwarn com.squareup.okhttp.** -keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# Android-Iconics (fontawesome-typeface) # Android-Iconics (fontawesome-typeface)
-keep class .R -keep class .R

7
app/src/main/AndroidManifest.xml

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="gr.thmmy.mthmmy" package="gr.thmmy.mthmmy"
android:installLocation="auto"> android:installLocation="auto">
@ -17,7 +18,10 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:requestLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="false" /> android:value="false" />
@ -67,6 +71,7 @@
android:name=".activities.LoginActivity" android:name=".activities.LoginActivity"
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop" android:launchMode="singleTop"
tools:ignore="LockedOrientationActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBar" android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustPan" /> android:windowSoftInputMode="adjustPan" />

60
app/src/main/assets/apache_libraries.html

@ -1,48 +1,12 @@
<html> <html>
<head>
<link rel="stylesheet" type="text/css" href="libraries_style.css" />
</head>
<head> <body>
<style>
body {
font-family: sans-serif;
background-color: #333333;
}
pre {
background-color: #3C3C3C;
color: #757575;
padding: 1em;
margin-left: 1em;
margin-right: 1em;
white-space: pre-wrap;
}
h4,
h5 {
display: inline;
padding: 1em;
}
a,
h4,
h5 {
color: #26A69A;
word-wrap: break-word;
}
li {
color: #26A69A;
}
</style>
</head>
<body>
<ul> <ul>
<li> <li>
<h5><a href="https://square.github.io/okhttp/">OkHttp</a>&nbsp;v3.14.2 (Copyright ©2019 Square, Inc.)</h5> <h5><a href="https://square.github.io/okhttp/">OkHttp</a>&nbsp;v3.12.12 (Copyright ©2019 Square, Inc.)</h5>
</li>
<li>
<h5><a href="https://square.github.io/picasso/">Picasso</a>&nbsp;v2.5.2 (Copyright ©2013 Square, Inc.)</h5>
</li> </li>
<li> <li>
<h5><a href="https://github.com/franmontiel/PersistentCookieJar">PersistentCookieJar</a>&nbsp;v1.0.1 (Copyright ©2016 Francisco José Montiel Navarro)</h5> <h5><a href="https://github.com/franmontiel/PersistentCookieJar">PersistentCookieJar</a>&nbsp;v1.0.1 (Copyright ©2016 Francisco José Montiel Navarro)</h5>
@ -53,6 +17,9 @@
<li> <li>
<h5><a href="https://github.com/mikepenz/MaterialDrawer">MaterialDrawer</a>&nbsp;v6.1.1 (Copyright ©2018 Mike Penz)</h5> <h5><a href="https://github.com/mikepenz/MaterialDrawer">MaterialDrawer</a>&nbsp;v6.1.1 (Copyright ©2018 Mike Penz)</h5>
</li> </li>
<li>
<h5><a href="https://github.com/chrisbanes/PhotoView">PhotoView</a>&nbsp;v2.3.0 (Copyright ©2018 Chris Banes)</h5>
</li>
<li> <li>
<h5><a href="https://github.com/mikepenz/Android-Iconics">Android-Iconics</a>&nbsp;v2.9.5 (Copyright ©2016 Mike Penz)</h5> <h5><a href="https://github.com/mikepenz/Android-Iconics">Android-Iconics</a>&nbsp;v2.9.5 (Copyright ©2016 Mike Penz)</h5>
</li> </li>
@ -72,7 +39,16 @@
<h5><a href="https://github.com/ajoberstar/grgit">Grgit</a>&nbsp;v3.0.0 (Copyright ©2018 Andrew Oberstar)</h5> <h5><a href="https://github.com/ajoberstar/grgit">Grgit</a>&nbsp;v3.0.0 (Copyright ©2018 Andrew Oberstar)</h5>
</li> </li>
<li> <li>
<h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.5</h5> <h5><a href="https://github.com/JodaOrg/joda-time">Joda-Time</a>&nbsp;v2.10.4 (Copyright ©2002-2019 Joda.org)</h5>
</li>
<li>
<h5><a href="https://github.com/powermock/powermock">PowerMock</a>&nbsp;v2.0.2</h5>
</li>
<li>
<h5><a href="https://github.com/sromku/android-storage">android-storage</a>&nbsp;v2.1.0</h5>
</li>
<li>
<h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.7</h5>
</li> </li>
</ul> </ul>

229
app/src/main/assets/epl_libraries.html

@ -0,0 +1,229 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="libraries_style.css" />
</head>
<body>
<ul>
<li>
<h5><a href="https://github.com/junit-team/junit4">JUnit</a>&nbsp;v4.12 (Copyright © 2002-2019, JUnit)</h5>
</li>
</ul>
<br/>
<h4>Eclipse Public License v1.0</h4>
<pre>
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from a
Contributor if it was added to the Program by such Contributor itself or anyone
acting on such Contributor's behalf. Contributions do not include additions to
the Program which: (i) are separate modules of software distributed in
conjunction with the Program under their own license agreement, and (ii) are
not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents " mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the
licenses to its Contributions set forth herein, no assurances are provided by
any Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility to
acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are
offered by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the
Program.
Contributors may not remove or alter any copyright notices contained within the
Program.
Each Contributor must identify itself as the originator of its Contribution, if
any, in a manner that reasonably allows subsequent Recipients to identify the
originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a manner
which does not create potential liability for other Contributors. Therefore, if
a Contributor includes the Program in a commercial product offering, such
Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
every other Contributor ("Indemnified Contributor") against any losses, damages
and costs (collectively "Losses") arising from claims, lawsuits and other legal
actions brought by a third party against the Indemnified Contributor to the
extent caused by the acts or omissions of such Commercial Contributor in
connection with its distribution of the Program in a commercial product
offering. The obligations in this section do not apply to any claims or Losses
relating to any actual or alleged intellectual property infringement. In order
to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
Contributor in writing of such claim, and b) allow the Commercial Contributor
to control, and cooperate with the Commercial Contributor in, the defense and
any related settlement negotiations. The Indemnified Contributor may
participate in any such claim at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If that
Commercial Contributor then makes performance claims, or offers warranties
related to Product X, those performance claims and warranties are such
Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a court
requires any other Contributor to pay any damages as a result, the Commercial
Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
Recipient is solely responsible for determining the appropriateness of using
and distributing the Program and assumes all risks associated with its exercise
of rights under this Agreement, including but not limited to the risks and
costs of program errors, compliance with applicable laws, damage to or loss of
data, programs or equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under applicable
law, it shall not affect the validity or enforceability of the remainder of the
terms of this Agreement, and without further action by the parties hereto, such
provision shall be reformed to the minimum extent necessary to make such
provision valid and enforceable.
If Recipient institutes patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other software or
hardware) infringes such Recipient's patent(s), then such Recipient's rights
granted under Section 2(b) shall terminate as of the date such litigation is
filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and does
not cure such failure in a reasonable period of time after becoming aware of
such noncompliance. If all Recipient's rights under this Agreement terminate,
Recipient agrees to cease use and distribution of the Program as soon as
reasonably practicable. However, Recipient's obligations under this Agreement
and any licenses granted by Recipient relating to the Program shall continue
and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to time.
No one other than the Agreement Steward has the right to modify this Agreement.
The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to
serve as the Agreement Steward to a suitable separate entity. Each new version
of the Agreement will be given a distinguishing version number. The Program
(including Contributions) may always be distributed subject to the version of
the Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly stated
in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to
the intellectual property of any Contributor under this Agreement, whether
expressly, by implication, estoppel or otherwise. All rights in the Program not
expressly granted under this Agreement are reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.
</pre>
</body>
</html>

28
app/src/main/assets/libraries_style.css

@ -0,0 +1,28 @@
body {
font-family: sans-serif;
background-color: #333333;
}
pre {
background-color: #3C3C3C;
color: #757575;
padding: 1em;
margin-left: 1em;
margin-right: 1em;
white-space: pre-wrap;
word-wrap: break-word;
}
h4, h5 {
display: inline;
padding: 1em;
}
a, h4, h5 {
color: #26A69A;
word-wrap: break-word;
}
li {
color: #26A69A;
}

42
app/src/main/assets/mit_libraries.html

@ -1,52 +1,22 @@
<html> <html>
<head> <head>
<style> <link rel="stylesheet" type="text/css" href="libraries_style.css" />
body {
font-family: sans-serif;
background-color: #333333;
}
pre {
background-color: #3C3C3C;
color: #757575;
padding: 1em;
margin-left: 1em;
margin-right: 1em;
white-space: pre-wrap;
}
h4,
h5 {
display: inline;
padding: 1em;
}
a,
h4,
h5 {
color: #26A69A;
word-wrap: break-word;
}
li {
color: #26A69A;
}
</style>
</head> </head>
<body> <body>
<ul> <ul>
<li> <li>
<h5><a href="https://jsoup.org//">jsoup</a>&nbsp;v1.10.3 (Copyright ©2009-2017, Jonathan Hedley &lt;jonathan@hedley.net&gt;)</h5> <h5><a href="https://jsoup.org">jsoup</a>&nbsp;v1.10.3 (Copyright ©2009-2017, Jonathan Hedley &lt;jonathan@hedley.net&gt;)</h5>
</li> </li>
<li> <li>
<h5><a href="https://github.com/koral--/android-gif-drawable">android-gif-drawable</a>&nbsp;v1.2.12 (Copyright ©2013 -2018 Karol Wrótniak, Droids on Roids)</h5> <h5><a href="https://github.com/koral--/android-gif-drawable">android-gif-drawable</a>&nbsp;v1.2.19 (Copyright ©2013 -2020 Karol Wrótniak, Droids on Roids)</h5>
</li> </li>
<li> <li>
<h5><a href="https://github.com/bignerdranch/expandable-recycler-view">Expandable RecyclerView</a>&nbsp;v3.0.0-RC1 (Copyright ©2015, Big Nerd Ranch)</h5> <h5><a href="https://github.com/bignerdranch/expandable-recycler-view">Expandable RecyclerView</a>&nbsp;v3.0.0-RC1 (Copyright ©2015, Big Nerd Ranch)</h5>
</li> </li>
<li>
<h5><a href="https://github.com/LachlanMcKee/timber-junit-rule">Timber JUnit-Rule</a>&nbsp;v1.0.1 (Copyright ©2017, Lachlan McKee)</h5>
</li>
</ul> </ul>

114
app/src/main/assets/other_libraries.html

@ -0,0 +1,114 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="libraries_style.css" />
</head>
<body>
<ul>
<li>
<h5><a href="https://github.com/bumptech/glide">Glide</a>&nbsp;v4.11.0</h5>
</li>
</ul>
<br/>
<h4>Glide License</h4>
<pre>
License for everything not in third_party and not otherwise marked:
Copyright 2014 Google, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of Google, Inc.
---------------------------------------------------------------------------------------------
License for third_party/disklrucache:
Copyright 2012 Jake Wharton
Copyright 2011 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
<a href="http://www.apache.org/licenses/">http://www.apache.org/licenses/</a>
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---------------------------------------------------------------------------------------------
License for third_party/gif_decoder:
Copyright (c) 2013 Xcellent Creations, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---------------------------------------------------------------------------------------------
License for third_party/gif_encoder/AnimatedGifEncoder.java and
third_party/gif_encoder/LZWEncoder.java:
No copyright asserted on the source code of this class. May be used for any
purpose, however, refer to the Unisys LZW patent for restrictions on use of
the associated LZWEncoder class. Please forward any corrections to
kweiner@fmsware.com.
-----------------------------------------------------------------------------
License for third_party/gif_encoder/NeuQuant.java
Copyright (c) 1994 Anthony Dekker
NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See
"Kohonen neural networks for optimal colour quantization" in "Network:
Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of
the algorithm.
Any party obtaining a copy of these files from the author, directly or
indirectly, is granted, free of charge, a full and unrestricted irrevocable,
world-wide, paid up, royalty-free, nonexclusive right and license to deal in
this software and documentation files (the "Software"), including without
limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons who
receive copies from any such party to do so, with the only requirement being
that this copyright notice remain intact.
</pre>
</body>
</html>

8
app/src/main/assets/style.css

@ -537,4 +537,12 @@ img
color: #a51111 !important; color: #a51111 !important;
} }
span[style="background-color: yellow;"]
{
color: black !important;
}
[style="color: white;"] > span[style="background-color: yellow;"]
{
color: white !important;
}

BIN
app/src/main/ic_launcher-playstore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

51
app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java

@ -1,7 +1,9 @@
package gr.thmmy.mthmmy.activities; package gr.thmmy.mthmmy.activities;
import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableString; import android.text.SpannableString;
@ -26,6 +28,7 @@ import gr.thmmy.mthmmy.BuildConfig;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.io.AssetUtils;
public class AboutActivity extends BaseActivity { public class AboutActivity extends BaseActivity {
private static final int TIME_INTERVAL = 1000; private static final int TIME_INTERVAL = 1000;
@ -39,6 +42,7 @@ public class AboutActivity extends BaseActivity {
private AlertDialog alertDialog; private AlertDialog alertDialog;
private FrameLayout easterEggImage; private FrameLayout easterEggImage;
@SuppressWarnings("ConstantConditions")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -134,29 +138,41 @@ public class AboutActivity extends BaseActivity {
hideEasterEgg(); hideEasterEgg();
} }
public void displayApacheLibraries(View v) { public void displayLibraries(View v) {
LayoutInflater inflater = LayoutInflater.from(this); String libraryType = v.getTag().toString();
WebView webView = (WebView) inflater.inflate(R.layout.dialog_licenses, coordinatorLayout, false); String title="", fileName="";
webView.loadUrl("file:///android_asset/apache_libraries.html"); switch(libraryType) {
int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.95); case "APACHE":
int height = (int) (getResources().getDisplayMetrics().heightPixels * 0.95); title=getString(R.string.apache_v2_0_libraries);
alertDialog = new AlertDialog.Builder(this, R.style.AppTheme_Dark_Dialog) fileName="apache_libraries.html";
.setTitle(getString(R.string.apache_v2_0_libraries)) break;
.setView(webView) case "MIT":
.setPositiveButton(android.R.string.ok, null) title=getString(R.string.the_mit_libraries);
.show(); fileName="mit_libraries.html";
if(alertDialog.getWindow()!=null) break;
alertDialog.getWindow().setLayout(width, height); case "EPL":
title=getString(R.string.epl_libraries);
fileName="epl_libraries.html";
break;
case "OTHER":
title=getString(R.string.other_libraries);
fileName="other_libraries.html";
break;
default:
break;
} }
public void displayMITLibraries(View v) { String htmlContent = AssetUtils.readFileToText(this,fileName);
LayoutInflater inflater = LayoutInflater.from(this); LayoutInflater inflater = LayoutInflater.from(this);
WebView webView = (WebView) inflater.inflate(R.layout.dialog_licenses, coordinatorLayout, false); WebView webView = (WebView) inflater.inflate(R.layout.dialog_licenses, coordinatorLayout, false);
webView.loadUrl("file:///android_asset/mit_libraries.html"); webView.setBackgroundColor(Color.argb(1, 255, 255, 255));
webView.loadDataWithBaseURL("file:///android_asset/", htmlContent, "text/html", "UTF-8", null);
int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.95); int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.95);
int height = (int) (getResources().getDisplayMetrics().heightPixels * 0.95); int height = (int) (getResources().getDisplayMetrics().heightPixels * 0.95);
alertDialog = new AlertDialog.Builder(this, R.style.AppTheme_Dark_Dialog) alertDialog = new AlertDialog.Builder(this, R.style.AppTheme_Dark_Dialog)
.setTitle(getString(R.string.the_mit_libraries)) .setTitle(title)
.setView(webView) .setView(webView)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton(android.R.string.ok, null)
.show(); .show();
@ -164,9 +180,10 @@ public class AboutActivity extends BaseActivity {
alertDialog.getWindow().setLayout(width, height); alertDialog.getWindow().setLayout(width, height);
} }
@SuppressLint("SourceLockedOrientationActivity")
private void showEasterEgg(){ private void showEasterEgg(){
if(getResources().getConfiguration().orientation==ActivityInfo.SCREEN_ORIENTATION_PORTRAIT){ if(getResources().getConfiguration().orientation==ActivityInfo.SCREEN_ORIENTATION_PORTRAIT){
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); //TODO: why?
appBar.setVisibility(View.INVISIBLE); appBar.setVisibility(View.INVISIBLE);
mainContent.setVisibility(View.INVISIBLE); mainContent.setVisibility(View.INVISIBLE);
easterEggImage.setVisibility(View.VISIBLE); easterEggImage.setVisibility(View.VISIBLE);

30
app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java

@ -21,6 +21,8 @@ import org.jsoup.select.Elements;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.LoginActivity; import gr.thmmy.mthmmy.activities.LoginActivity;
@ -96,7 +98,9 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
} }
thisPageBookmark = new Bookmark(boardTitle, ThmmyPage.getBoardId(boardUrl), true); thisPageBookmark = new Bookmark(boardTitle, ThmmyPage.getBoardId(boardUrl), true);
if (boardTitle != null && !Objects.equals(boardTitle, ""))
setBoardBookmark(findViewById(R.id.bookmark)); setBoardBookmark(findViewById(R.id.bookmark));
createDrawer(); createDrawer();
progressBar = findViewById(R.id.progressBar); progressBar = findViewById(R.id.progressBar);
@ -156,9 +160,9 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
@Override @Override
public void onLoadMore() { public void onLoadMore() {
if (pagesLoaded < numberOfPages && parsedTopics.get(parsedTopics.size() - 1) != null) { if (pagesLoaded < numberOfPages && !parsedTopics.isEmpty() && parsedTopics.get(parsedTopics.size() - 1) != null) {
parsedTopics.add(null); parsedTopics.add(null);
boardAdapter.notifyItemInserted(parsedSubBoards.size() + parsedTopics.size()); boardAdapter.notifyItemInserted(parsedSubBoards.size() + parsedTopics.size()); // This gets a warning and should be changed
//Load data //Load data
boardTask = new BoardTask(); boardTask = new BoardTask();
@ -275,7 +279,7 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
if (topicRows != null && !topicRows.isEmpty()) { if (topicRows != null && !topicRows.isEmpty()) {
for (Element topicRow : topicRows) { for (Element topicRow : topicRows) {
if (!Objects.equals(topicRow.className(), "titlebg")) { if (!Objects.equals(topicRow.className(), "titlebg")) {
String pTopicUrl, pSubject, pStartedBy, pLastPost, pLastPostUrl, pStats; String pTopicUrl, pSubject, pStarter, pLastUser="", pLastPostDateTime="00:00:00", pLastPost, pLastPostUrl, pStats;
boolean pLocked = false, pSticky = false, pUnread = false; boolean pLocked = false, pSticky = false, pUnread = false;
Elements topicColumns = topicRow.select(">td"); Elements topicColumns = topicRow.select(">td");
{ {
@ -290,21 +294,21 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
if (column.select("a[id^=newicon]").first() != null) if (column.select("a[id^=newicon]").first() != null)
pUnread = true; pUnread = true;
} }
pStartedBy = topicColumns.get(3).text(); pStarter = topicColumns.get(3).text();
pStats = "Replies: " + topicColumns.get(4).text() + ", Views: " + topicColumns.get(5).text(); pStats = "Replies: " + topicColumns.get(4).text() + ", Views: " + topicColumns.get(5).text();
pLastPost = topicColumns.last().text(); pLastPost = topicColumns.last().text();
if (pLastPost.contains("by")) { Pattern pattern = Pattern.compile("(.+)\\s(by|από)\\s(.+)$");
pLastPost = pLastPost.substring(0, pLastPost.indexOf("by")) + Matcher matcher = pattern.matcher(pLastPost);
"\n" + pLastPost.substring(pLastPost.indexOf("by")); if (matcher.find()){
} else if (pLastPost.contains("από")) { pLastPostDateTime = matcher.group(1);
pLastPost = pLastPost.substring(0, pLastPost.indexOf("από")) + pLastUser = matcher.group(3);
"\n" + pLastPost.substring(pLastPost.indexOf("από"));
} else {
Timber.wtf("Board parsing about to fail. pLastPost came with: %s", pLastPost);
} }
else
throw new ParseException("Parsing failed (pLastPost came with: \"" + pLastPost + "\")");
pLastPostUrl = topicColumns.last().select("a:has(img)").first().attr("href"); pLastPostUrl = topicColumns.last().select("a:has(img)").first().attr("href");
tempTopics.add(new Topic(pTopicUrl, pSubject, pStartedBy, pLastPost, pLastPostUrl, tempTopics.add(new Topic(pTopicUrl, pSubject, pStarter, pLastUser, pLastPostDateTime, pLastPostUrl,
pStats, pLocked, pSticky, pUnread)); pStats, pLocked, pSticky, pUnread));
} }
} }

8
app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java

@ -39,8 +39,8 @@ class BoardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final int VIEW_TYPE_LOADING = 4; private final int VIEW_TYPE_LOADING = 4;
private final Context context; private final Context context;
private ArrayList<Board> parsedSubBoards = new ArrayList<>(); private ArrayList<Board> parsedSubBoards;
private ArrayList<Topic> parsedTopics = new ArrayList<>(); private ArrayList<Topic> parsedTopics;
private final ArrayList<Boolean> boardExpandableVisibility = new ArrayList<>(); private final ArrayList<Boolean> boardExpandableVisibility = new ArrayList<>();
private final ArrayList<Boolean> topicExpandableVisibility = new ArrayList<>(); private final ArrayList<Boolean> topicExpandableVisibility = new ArrayList<>();
@ -187,7 +187,7 @@ class BoardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
topicViewHolder.topicRow.setOnClickListener(view -> { topicViewHolder.topicRow.setOnClickListener(view -> {
Intent intent = new Intent(context, TopicActivity.class); Intent intent = new Intent(context, TopicActivity.class);
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(BUNDLE_TOPIC_URL, topic.getUrl()); extras.putString(BUNDLE_TOPIC_URL, topic.getTopicUrl());
extras.putString(BUNDLE_TOPIC_TITLE, topic.getSubject()); extras.putString(BUNDLE_TOPIC_TITLE, topic.getSubject());
intent.putExtras(extras); intent.putExtras(extras);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
@ -232,7 +232,7 @@ class BoardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
topicViewHolder.topicSubject.setText(lockedSticky); topicViewHolder.topicSubject.setText(lockedSticky);
topicViewHolder.topicStartedBy.setText(context.getString(R.string.topic_started_by, topic.getStarter())); topicViewHolder.topicStartedBy.setText(context.getString(R.string.topic_started_by, topic.getStarter()));
topicViewHolder.topicStats.setText(topic.getStats()); topicViewHolder.topicStats.setText(topic.getStats());
topicViewHolder.topicLastPost.setText(context.getString(R.string.topic_last_post, topic.getLastPostDateAndTime())); topicViewHolder.topicLastPost.setText(context.getString(R.string.topic_last_post, topic.getLastPostDateTime(), topic.getLastUser()));
topicViewHolder.topicLastPost.setOnClickListener(view -> { topicViewHolder.topicLastPost.setOnClickListener(view -> {
Intent intent = new Intent(context, TopicActivity.class); Intent intent = new Intent(context, TopicActivity.class);
Bundle extras = new Bundle(); Bundle extras = new Bundle();

40
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksActivity.java

@ -29,6 +29,9 @@ import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL;
//TODO proper handling with adapter etc. //TODO proper handling with adapter etc.
//TODO after clicking bookmark and then back button should return to this activity //TODO after clicking bookmark and then back button should return to this activity
public class BookmarksActivity extends BaseActivity { public class BookmarksActivity extends BaseActivity {
private static final String TOPIC_URL = "https://www.thmmy.gr/smf/index.php?topic=";
private static final String BOARD_URL = "https://www.thmmy.gr/smf/index.php?board=";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -48,8 +51,8 @@ public class BookmarksActivity extends BaseActivity {
//Creates the adapter that will return a fragment for each section of the activity //Creates the adapter that will return a fragment for each section of the activity
SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
sectionsPagerAdapter.addFragment(BookmarksTopicFragment.newInstance(1, Bookmark.arrayListToString(getTopicsBookmarked())), "Topics"); sectionsPagerAdapter.addFragment(BookmarksFragment.newInstance(1, Bookmark.arrayListToString(getTopicsBookmarked()), BookmarksFragment.Type.TOPIC), "Topics");
sectionsPagerAdapter.addFragment(BookmarksBoardFragment.newInstance(2, Bookmark.arrayListToString(getBoardsBookmarked())), "Boards"); sectionsPagerAdapter.addFragment(BookmarksFragment.newInstance(2, Bookmark.arrayListToString(getBoardsBookmarked()), BookmarksFragment.Type.BOARD), "Boards");
//Sets up the ViewPager with the sections adapter. //Sets up the ViewPager with the sections adapter.
ViewPager viewPager = findViewById(R.id.bookmarks_container); ViewPager viewPager = findViewById(R.id.bookmarks_container);
@ -65,44 +68,57 @@ public class BookmarksActivity extends BaseActivity {
super.onResume(); super.onResume();
} }
public boolean onTopicInteractionListener(String interactionType, Bookmark bookmarkedTopic) { public boolean onFragmentRowInteractionListener(BookmarksFragment.Type type, String interactionType, Bookmark bookmark) {
if(type== BookmarksFragment.Type.TOPIC)
return onTopicInteractionListener(interactionType, bookmark);
else if (type==BookmarksFragment.Type.BOARD)
return onBoardInteractionListener(interactionType, bookmark);
return false;
}
private boolean onTopicInteractionListener(String interactionType, Bookmark bookmarkedTopic) {
switch (interactionType) { switch (interactionType) {
case BookmarksTopicFragment.INTERACTION_CLICK_TOPIC_BOOKMARK: case BookmarksFragment.INTERACTION_CLICK_TOPIC_BOOKMARK:
Intent intent = new Intent(BookmarksActivity.this, TopicActivity.class); Intent intent = new Intent(BookmarksActivity.this, TopicActivity.class);
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(BUNDLE_TOPIC_URL, "https://www.thmmy.gr/smf/index.php?topic=" extras.putString(BUNDLE_TOPIC_URL, TOPIC_URL
+ bookmarkedTopic.getId() + "." + 2147483647); + bookmarkedTopic.getId() + "." + 2147483647);
extras.putString(BUNDLE_TOPIC_TITLE, bookmarkedTopic.getTitle()); extras.putString(BUNDLE_TOPIC_TITLE, bookmarkedTopic.getTitle());
intent.putExtras(extras); intent.putExtras(extras);
startActivity(intent); startActivity(intent);
break; break;
case BookmarksTopicFragment.INTERACTION_TOGGLE_TOPIC_NOTIFICATION: case BookmarksFragment.INTERACTION_TOGGLE_TOPIC_NOTIFICATION:
return toggleNotification(bookmarkedTopic); return toggleNotification(bookmarkedTopic);
case BookmarksTopicFragment.INTERACTION_REMOVE_TOPIC_BOOKMARK: case BookmarksFragment.INTERACTION_REMOVE_TOPIC_BOOKMARK:
removeBookmark(bookmarkedTopic); removeBookmark(bookmarkedTopic);
Toast.makeText(BookmarksActivity.this, "Bookmark removed", Toast.LENGTH_SHORT).show(); Toast.makeText(BookmarksActivity.this, "Bookmark removed", Toast.LENGTH_SHORT).show();
break; break;
default:
break;
} }
return true; return true;
} }
public boolean onBoardInteractionListener(String interactionType, Bookmark bookmarkedBoard) { private boolean onBoardInteractionListener(String interactionType, Bookmark bookmarkedBoard) {
switch (interactionType) { switch (interactionType) {
case BookmarksBoardFragment.INTERACTION_CLICK_BOARD_BOOKMARK: case BookmarksFragment.INTERACTION_CLICK_BOARD_BOOKMARK:
Intent intent = new Intent(BookmarksActivity.this, BoardActivity.class); Intent intent = new Intent(BookmarksActivity.this, BoardActivity.class);
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(BUNDLE_BOARD_URL, "https://www.thmmy.gr/smf/index.php?board=" extras.putString(BUNDLE_BOARD_URL, BOARD_URL
+ bookmarkedBoard.getId() + ".0"); + bookmarkedBoard.getId() + ".0");
extras.putString(BUNDLE_BOARD_TITLE, bookmarkedBoard.getTitle()); extras.putString(BUNDLE_BOARD_TITLE, bookmarkedBoard.getTitle());
intent.putExtras(extras); intent.putExtras(extras);
startActivity(intent); startActivity(intent);
break; break;
case BookmarksBoardFragment.INTERACTION_TOGGLE_BOARD_NOTIFICATION: case BookmarksFragment.INTERACTION_TOGGLE_BOARD_NOTIFICATION:
return toggleNotification(bookmarkedBoard); return toggleNotification(bookmarkedBoard);
case BookmarksBoardFragment.INTERACTION_REMOVE_BOARD_BOOKMARK: case BookmarksFragment.INTERACTION_REMOVE_BOARD_BOOKMARK:
removeBookmark(bookmarkedBoard); removeBookmark(bookmarkedBoard);
Toast.makeText(BookmarksActivity.this, "Bookmark removed", Toast.LENGTH_SHORT).show(); Toast.makeText(BookmarksActivity.this, "Bookmark removed", Toast.LENGTH_SHORT).show();
break; break;
default:
break;
} }
return true; return true;
} }

114
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksBoardFragment.java → app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java

@ -1,7 +1,6 @@
package gr.thmmy.mthmmy.activities.bookmarks; package gr.thmmy.mthmmy.activities.bookmarks;
import android.app.Activity; import android.app.Activity;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -21,38 +20,55 @@ import java.util.ArrayList;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.model.Bookmark; import gr.thmmy.mthmmy.model.Bookmark;
/** public class BookmarksFragment extends Fragment {
* A {@link Fragment} subclass. enum Type {TOPIC, BOARD}
* Use the {@link BookmarksBoardFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class BookmarksBoardFragment extends Fragment {
private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER"; private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER";
private static final String ARG_BOARD_BOOKMARKS = "BOARD_BOOKMARKS"; private static final String ARG_BOOKMARKS = "BOOKMARKS";
static final String INTERACTION_CLICK_TOPIC_BOOKMARK = "CLICK_TOPIC_BOOKMARK";
static final String INTERACTION_TOGGLE_TOPIC_NOTIFICATION = "TOGGLE_TOPIC_NOTIFICATION";
static final String INTERACTION_REMOVE_TOPIC_BOOKMARK = "REMOVE_TOPIC_BOOKMARK";
static final String INTERACTION_CLICK_BOARD_BOOKMARK = "CLICK_BOARD_BOOKMARK"; static final String INTERACTION_CLICK_BOARD_BOOKMARK = "CLICK_BOARD_BOOKMARK";
static final String INTERACTION_TOGGLE_BOARD_NOTIFICATION = "TOGGLE_BOARD_NOTIFICATION"; static final String INTERACTION_TOGGLE_BOARD_NOTIFICATION = "TOGGLE_BOARD_NOTIFICATION";
static final String INTERACTION_REMOVE_BOARD_BOOKMARK= "REMOVE_BOARD_BOOKMARK"; static final String INTERACTION_REMOVE_BOARD_BOOKMARK= "REMOVE_BOARD_BOOKMARK";
private ArrayList<Bookmark> boardBookmarks = null; private TextView nothingBookmarkedTextView;
private ArrayList<Bookmark> bookmarks = null;
private Type type;
private String interactionClick, interactionToggle, interactionRemove;
private Drawable notificationsEnabledButtonImage;
private Drawable notificationsDisabledButtonImage;
private static Drawable notificationsEnabledButtonImage; public BookmarksFragment() {/* Required empty public constructor */}
private static Drawable notificationsDisabledButtonImage;
// Required empty public constructor private BookmarksFragment(Type type) {
public BookmarksBoardFragment() { } this.type=type;
if(type==Type.TOPIC){
this.interactionClick=INTERACTION_CLICK_TOPIC_BOOKMARK;
this.interactionToggle=INTERACTION_TOGGLE_TOPIC_NOTIFICATION;
this.interactionRemove=INTERACTION_REMOVE_TOPIC_BOOKMARK;
}
else if (type==Type.BOARD){
this.interactionClick=INTERACTION_CLICK_BOARD_BOOKMARK;
this.interactionToggle=INTERACTION_TOGGLE_BOARD_NOTIFICATION;
this.interactionRemove=INTERACTION_REMOVE_BOARD_BOOKMARK;
}
}
/** /**
* Use ONLY this factory method to create a new instance of * Use ONLY this factory method to create a new instance of
* this fragment using the provided parameters. * the desired fragment using the provided parameters.
* *
* @return A new instance of fragment Forum. * @return A new instance of fragment Forum.
*/ */
public static BookmarksBoardFragment newInstance(int sectionNumber, String boardBookmarks) { protected static BookmarksFragment newInstance(int sectionNumber, String bookmarks, Type type) {
BookmarksBoardFragment fragment = new BookmarksBoardFragment(); BookmarksFragment fragment = new BookmarksFragment(type);
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber); args.putInt(ARG_SECTION_NUMBER, sectionNumber);
args.putString(ARG_BOARD_BOOKMARKS, boardBookmarks); args.putString(ARG_BOOKMARKS, bookmarks);
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
@ -61,9 +77,9 @@ public class BookmarksBoardFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (getArguments() != null) { if (getArguments() != null) {
String bundledBoardBookmarks = getArguments().getString(ARG_BOARD_BOOKMARKS); String bundledBookmarks = getArguments().getString(ARG_BOOKMARKS);
if (bundledBoardBookmarks != null) { if (bundledBookmarks != null) {
boardBookmarks = Bookmark.stringToArrayList(bundledBoardBookmarks); bookmarks = Bookmark.stringToArrayList(bundledBookmarks);
} }
} }
@ -83,74 +99,68 @@ public class BookmarksBoardFragment extends Fragment {
Bundle savedInstanceState) { Bundle savedInstanceState) {
// Inflates the layout for this fragment // Inflates the layout for this fragment
final View rootView = layoutInflater.inflate(R.layout.fragment_bookmarks, container, false); final View rootView = layoutInflater.inflate(R.layout.fragment_bookmarks, container, false);
//bookmarks_board_container //bookmarks container
final LinearLayout bookmarksLinearView = rootView.findViewById(R.id.bookmarks_container); final LinearLayout bookmarksLinearView = rootView.findViewById(R.id.bookmarks_container);
nothingBookmarkedTextView = rootView.findViewById(R.id.nothing_bookmarked);
if(this.boardBookmarks != null && !this.boardBookmarks.isEmpty()) { if(this.bookmarks != null && !this.bookmarks.isEmpty()) {
for (final Bookmark bookmarkedBoard : boardBookmarks) { hideNothingBookmarked();
if (bookmarkedBoard != null && bookmarkedBoard.getTitle() != null) { for (final Bookmark bookmark : bookmarks) {
if (bookmark != null && bookmark.getTitle() != null) {
final LinearLayout row = (LinearLayout) layoutInflater.inflate( final LinearLayout row = (LinearLayout) layoutInflater.inflate(
R.layout.fragment_bookmarks_row, bookmarksLinearView, false); R.layout.fragment_bookmarks_row, bookmarksLinearView, false);
row.setOnClickListener(view -> { row.setOnClickListener(view -> {
Activity activity = getActivity(); Activity activity = getActivity();
if (activity instanceof BookmarksActivity){ if (activity instanceof BookmarksActivity)
((BookmarksActivity) activity).onBoardInteractionListener(INTERACTION_CLICK_BOARD_BOOKMARK, bookmarkedBoard); ((BookmarksActivity) activity).onFragmentRowInteractionListener(type, interactionClick, bookmark);
}
}); });
((TextView) row.findViewById(R.id.bookmark_title)).setText(bookmarkedBoard.getTitle()); ((TextView) row.findViewById(R.id.bookmark_title)).setText(bookmark.getTitle());
final ImageButton notificationsEnabledButton = row.findViewById(R.id.toggle_notification); final ImageButton notificationsEnabledButton = row.findViewById(R.id.toggle_notification);
if (!bookmarkedBoard.isNotificationsEnabled()) { if (!bookmark.isNotificationsEnabled()) {
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage); notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
} }
notificationsEnabledButton.setOnClickListener(view -> { notificationsEnabledButton.setOnClickListener(view -> {
Activity activity = getActivity(); Activity activity = getActivity();
if (activity instanceof BookmarksActivity) { if (activity instanceof BookmarksActivity) {
if (((BookmarksActivity) activity).onBoardInteractionListener(INTERACTION_TOGGLE_BOARD_NOTIFICATION, bookmarkedBoard)) { if (((BookmarksActivity) activity).onFragmentRowInteractionListener(type, interactionToggle, bookmark))
notificationsEnabledButton.setImageDrawable(notificationsEnabledButtonImage); notificationsEnabledButton.setImageDrawable(notificationsEnabledButtonImage);
} else { else
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage); notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
} }
}
}); });
(row.findViewById(R.id.remove_bookmark)).setOnClickListener(view -> { (row.findViewById(R.id.remove_bookmark)).setOnClickListener(view -> {
Activity activity = getActivity(); Activity activity = getActivity();
if (activity instanceof BookmarksActivity){ if (activity instanceof BookmarksActivity){
((BookmarksActivity) activity).onBoardInteractionListener(INTERACTION_REMOVE_BOARD_BOOKMARK, bookmarkedBoard); ((BookmarksActivity) activity).onFragmentRowInteractionListener(type, interactionRemove, bookmark);
boardBookmarks.remove(bookmarkedBoard); bookmarks.remove(bookmark);
} }
row.setVisibility(View.GONE); row.setVisibility(View.GONE);
if (boardBookmarks.isEmpty()){ if (bookmarks.isEmpty()){
bookmarksLinearView.addView(bookmarksListEmptyMessage()); showNothingBookmarked();
} }
}); });
bookmarksLinearView.addView(row); bookmarksLinearView.addView(row);
} }
} }
} else } else
bookmarksLinearView.addView(bookmarksListEmptyMessage()); showNothingBookmarked();
return rootView; return rootView;
} }
private TextView bookmarksListEmptyMessage() {
TextView emptyBookmarksCategory = new TextView(this.getContext()); private void showNothingBookmarked() {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( if(nothingBookmarkedTextView!=null)
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); nothingBookmarkedTextView.setVisibility(View.VISIBLE);
params.setMargins(0, 12, 0, 0);
emptyBookmarksCategory.setLayoutParams(params);
emptyBookmarksCategory.setText(getString(R.string.empty_board_bookmarks));
emptyBookmarksCategory.setTypeface(emptyBookmarksCategory.getTypeface(), Typeface.BOLD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
emptyBookmarksCategory.setTextColor(this.getContext().getColor(R.color.primary_text));
else {
//noinspection deprecation
emptyBookmarksCategory.setTextColor(this.getContext().getResources().getColor(R.color.primary_text));
} }
emptyBookmarksCategory.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
return emptyBookmarksCategory; private void hideNothingBookmarked(){
if(nothingBookmarkedTextView!=null)
nothingBookmarkedTextView.setVisibility(View.INVISIBLE);
} }
} }

158
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksTopicFragment.java

@ -1,158 +0,0 @@
package gr.thmmy.mthmmy.activities.bookmarks;
import android.app.Activity;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import java.util.ArrayList;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.model.Bookmark;
/**
* A {@link Fragment} subclass.
* Use the {@link BookmarksTopicFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class BookmarksTopicFragment extends Fragment {
private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER";
private static final String ARG_TOPIC_BOOKMARKS = "TOPIC_BOOKMARKS";
static final String INTERACTION_CLICK_TOPIC_BOOKMARK = "CLICK_TOPIC_BOOKMARK";
static final String INTERACTION_TOGGLE_TOPIC_NOTIFICATION = "TOGGLE_TOPIC_NOTIFICATION";
static final String INTERACTION_REMOVE_TOPIC_BOOKMARK = "REMOVE_TOPIC_BOOKMARK";
ArrayList<Bookmark> topicBookmarks = null;
private static Drawable notificationsEnabledButtonImage;
private static Drawable notificationsDisabledButtonImage;
// Required empty public constructor
public BookmarksTopicFragment() {
}
/**
* Use ONLY this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @return A new instance of fragment Forum.
*/
public static BookmarksTopicFragment newInstance(int sectionNumber, String topicBookmarks) {
BookmarksTopicFragment fragment = new BookmarksTopicFragment();
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
args.putString(ARG_TOPIC_BOOKMARKS, topicBookmarks);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
String bundledTopicBookmarks = getArguments().getString(ARG_TOPIC_BOOKMARKS);
if (bundledTopicBookmarks != null) {
topicBookmarks = Bookmark.stringToArrayList(bundledTopicBookmarks);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
notificationsEnabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_on, null);
else
notificationsEnabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_on, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
notificationsDisabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_off, null);
else
notificationsDisabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_off, null);
}
@Override
public View onCreateView(@NonNull LayoutInflater layoutInflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflates the layout for this fragment
final View rootView = layoutInflater.inflate(R.layout.fragment_bookmarks, container, false);
//bookmarks_topic_container
final LinearLayout bookmarksLinearView = rootView.findViewById(R.id.bookmarks_container);
if(this.topicBookmarks != null && !this.topicBookmarks.isEmpty()) {
for (final Bookmark bookmarkedTopic : topicBookmarks) {
if (bookmarkedTopic != null && bookmarkedTopic.getTitle() != null) {
final LinearLayout row = (LinearLayout) layoutInflater.inflate(
R.layout.fragment_bookmarks_row, bookmarksLinearView, false);
row.setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity) {
((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_CLICK_TOPIC_BOOKMARK, bookmarkedTopic);
}
});
((TextView) row.findViewById(R.id.bookmark_title)).setText(bookmarkedTopic.getTitle());
final ImageButton notificationsEnabledButton = row.findViewById(R.id.toggle_notification);
if (!bookmarkedTopic.isNotificationsEnabled()) {
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
}
notificationsEnabledButton.setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity) {
if (((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_TOGGLE_TOPIC_NOTIFICATION, bookmarkedTopic)) {
notificationsEnabledButton.setImageDrawable(notificationsEnabledButtonImage);
} else {
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage);
}
}
});
(row.findViewById(R.id.remove_bookmark)).setOnClickListener(view -> {
Activity activity = getActivity();
if (activity instanceof BookmarksActivity) {
((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_REMOVE_TOPIC_BOOKMARK, bookmarkedTopic);
topicBookmarks.remove(bookmarkedTopic);
}
row.setVisibility(View.GONE);
if (topicBookmarks.isEmpty()){
bookmarksLinearView.addView(bookmarksListEmptyMessage());
}
});
bookmarksLinearView.addView(row);
}
}
} else {
bookmarksLinearView.addView(bookmarksListEmptyMessage());
}
return rootView;
}
private TextView bookmarksListEmptyMessage() {
TextView emptyBookmarksCategory = new TextView(this.getContext());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 12, 0, 0);
emptyBookmarksCategory.setLayoutParams(params);
emptyBookmarksCategory.setText(getString(R.string.empty_topic_bookmarks));
emptyBookmarksCategory.setTypeface(emptyBookmarksCategory.getTypeface(), Typeface.BOLD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
emptyBookmarksCategory.setTextColor(this.getContext().getColor(R.color.primary_text));
} else {
//noinspection deprecation
emptyBookmarksCategory.setTextColor(this.getContext().getResources().getColor(R.color.primary_text));
}
emptyBookmarksCategory.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
return emptyBookmarksCategory;
}
}

4
app/src/main/java/gr/thmmy/mthmmy/activities/create_content/CreateContentActivity.java

@ -15,9 +15,9 @@ import com.google.android.material.textfield.TextInputLayout;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.settings.SettingsActivity; import gr.thmmy.mthmmy.activities.settings.SettingsActivity;
import gr.thmmy.mthmmy.base.BaseActivity; 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.session.SessionManager;
import gr.thmmy.mthmmy.views.editorview.EditorView;
import gr.thmmy.mthmmy.views.editorview.EmojiKeyboard;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber; import timber.log.Timber;

4
app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java

@ -159,7 +159,7 @@ public class DownloadsActivity extends BaseActivity implements DownloadsAdapter.
public void onLoadMore() { public void onLoadMore() {
if (pagesLoaded < numberOfPages) { if (pagesLoaded < numberOfPages) {
parsedDownloads.add(null); parsedDownloads.add(null);
downloadsAdapter.notifyItemInserted(parsedDownloads.size()); downloadsAdapter.notifyItemInserted(parsedDownloads.size()); //This gets a warning - change it!
//Load data //Load data
parseDownloadPageTask = new ParseDownloadPageTask(); parseDownloadPageTask = new ParseDownloadPageTask();
@ -287,7 +287,7 @@ public class DownloadsActivity extends BaseActivity implements DownloadsAdapter.
OkHttpClient client = BaseApplication.getInstance().getClient(); OkHttpClient client = BaseApplication.getInstance().getClient();
String fileName = null; String fileName = null;
try { try {
Response response = client.newCall(new Request.Builder().url(download.getUrl()).build()).execute(); Response response = client.newCall(new Request.Builder().url(download.getUrl()).head().build()).execute();
String contentDisposition = response.headers("Content-Disposition").toString(); //check if link provides an attachment String contentDisposition = response.headers("Content-Disposition").toString(); //check if link provides an attachment
if (contentDisposition.contains("attachment")) if (contentDisposition.contains("attachment"))
fileName = contentDisposition.split("\"")[1]; fileName = contentDisposition.split("\"")[1];

17
app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java

@ -34,7 +34,7 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final int VIEW_TYPE_LOADING = 1; private final int VIEW_TYPE_LOADING = 1;
private final Context context; private final Context context;
private ArrayList<Download> parsedDownloads = new ArrayList<>(); private ArrayList<Download> parsedDownloads;
private final ArrayList<Boolean> downloadExpandableVisibility = new ArrayList<>(); private final ArrayList<Boolean> downloadExpandableVisibility = new ArrayList<>();
DownloadsAdapter(Context context, ArrayList<Download> parsedDownloads) { DownloadsAdapter(Context context, ArrayList<Download> parsedDownloads) {
@ -77,9 +77,7 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} }
if (download.getType() == Download.DownloadItemType.DOWNLOADS_CATEGORY) { if (download.getType() == Download.DownloadItemType.DOWNLOADS_CATEGORY) {
downloadViewHolder.downloadRow.setOnClickListener(new View.OnClickListener() { downloadViewHolder.downloadRow.setOnClickListener(view -> {
@Override
public void onClick(View view) {
Intent intent = new Intent(context, DownloadsActivity.class); Intent intent = new Intent(context, DownloadsActivity.class);
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(BUNDLE_DOWNLOADS_URL, download.getUrl()); extras.putString(BUNDLE_DOWNLOADS_URL, download.getUrl());
@ -87,7 +85,6 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
intent.putExtras(extras); intent.putExtras(extras);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK); intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent); context.startActivity(intent);
}
}); });
if (downloadExpandableVisibility.get(downloadViewHolder.getAdapterPosition())) { if (downloadExpandableVisibility.get(downloadViewHolder.getAdapterPosition())) {
@ -97,9 +94,7 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
downloadViewHolder.informationExpandable.setVisibility(View.GONE); downloadViewHolder.informationExpandable.setVisibility(View.GONE);
downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_down_accent_24dp); downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_down_accent_24dp);
} }
downloadViewHolder.informationExpandableBtn.setOnClickListener(new View.OnClickListener() { downloadViewHolder.informationExpandableBtn.setOnClickListener(view -> {
@Override
public void onClick(View view) {
final boolean visible = downloadExpandableVisibility.get(downloadViewHolder. final boolean visible = downloadExpandableVisibility.get(downloadViewHolder.
getAdapterPosition()); getAdapterPosition());
if (visible) { if (visible) {
@ -110,7 +105,6 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_up_accent_24dp); downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_up_accent_24dp);
} }
downloadExpandableVisibility.set(downloadViewHolder.getAdapterPosition(), !visible); downloadExpandableVisibility.set(downloadViewHolder.getAdapterPosition(), !visible);
}
}); });
downloadViewHolder.title.setTypeface(Typeface.createFromAsset(context.getAssets() downloadViewHolder.title.setTypeface(Typeface.createFromAsset(context.getAssets()
, "fonts/fontawesome-webfont.ttf")); , "fonts/fontawesome-webfont.ttf"));
@ -124,16 +118,13 @@ class DownloadsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
downloadViewHolder.title.setText(tmp); downloadViewHolder.title.setText(tmp);
} }
} else { } else {
downloadViewHolder.downloadRow.setOnClickListener(new View.OnClickListener() { downloadViewHolder.downloadRow.setOnClickListener(view -> {
@Override
public void onClick(View view) {
try { try {
((BaseActivity) context).downloadFile(new ThmmyFile( ((BaseActivity) context).downloadFile(new ThmmyFile(
new URL(download.getUrl()), download.getFileName(), null)); new URL(download.getUrl()), download.getFileName(), null));
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
e.printStackTrace(); e.printStackTrace();
} }
}
}); });
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

15
app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java

@ -30,10 +30,10 @@ import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.Board; import gr.thmmy.mthmmy.model.Board;
import gr.thmmy.mthmmy.model.Category; import gr.thmmy.mthmmy.model.Category;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -158,19 +158,24 @@ public class ForumFragment extends BaseFragment {
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (forumTask != null && forumTask.getStatus() != AsyncTask.Status.RUNNING) if (forumTask!=null){
try{
if(forumTask.isRunning())
forumTask.cancel(true); forumTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
} }
public interface ForumFragmentInteractionListener extends FragmentInteractionListener { public interface ForumFragmentInteractionListener extends FragmentInteractionListener {
void onForumFragmentInteraction(Board board); void onForumFragmentInteraction(Board board);
} }
public void onForumTaskStarted() { private void onForumTaskStarted() {
progressBar.setVisibility(ProgressBar.VISIBLE); progressBar.setVisibility(ProgressBar.VISIBLE);
} }
public void onForumTaskFinished(int resultCode, ArrayList<Category> fetchedCategories) { private void onForumTaskFinished(int resultCode, ArrayList<Category> fetchedCategories) {
if (resultCode == NetworkResultCodes.SUCCESSFUL) { if (resultCode == NetworkResultCodes.SUCCESSFUL) {
categories.clear(); categories.clear();
categories.addAll(fetchedCategories); categories.addAll(fetchedCategories);
@ -191,7 +196,7 @@ public class ForumFragment extends BaseFragment {
private class ForumTask extends NewParseTask<ArrayList<Category>> { private class ForumTask extends NewParseTask<ArrayList<Category>> {
private HttpUrl forumUrl = SessionManager.forumUrl; //may change upon collapse/expand private HttpUrl forumUrl = SessionManager.forumUrl; //may change upon collapse/expand
public ForumTask(OnTaskStartedListener onTaskStartedListener, ForumTask(OnTaskStartedListener onTaskStartedListener,
OnNetworkTaskFinishedListener<ArrayList<Category>> onParseTaskFinishedListener) { OnNetworkTaskFinishedListener<ArrayList<Category>> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener); super(onTaskStartedListener, onParseTaskFinishedListener);
} }

38
app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java

@ -1,6 +1,5 @@
package gr.thmmy.mthmmy.activities.main.recent; package gr.thmmy.mthmmy.activities.main.recent;
import android.content.Context;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -12,8 +11,11 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List; import java.util.List;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary; import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.views.RelativeTimeTextView;
import timber.log.Timber;
/** /**
@ -21,12 +23,10 @@ import gr.thmmy.mthmmy.model.TopicSummary;
* specified {@link RecentFragment.RecentFragmentInteractionListener}. * specified {@link RecentFragment.RecentFragmentInteractionListener}.
*/ */
class RecentAdapter extends RecyclerView.Adapter<RecentAdapter.ViewHolder> { class RecentAdapter extends RecyclerView.Adapter<RecentAdapter.ViewHolder> {
private final Context context;
private final List<TopicSummary> recentList; private final List<TopicSummary> recentList;
private final RecentFragment.RecentFragmentInteractionListener mListener; private final RecentFragment.RecentFragmentInteractionListener mListener;
RecentAdapter(Context context, @NonNull List<TopicSummary> topicSummaryList, BaseFragment.FragmentInteractionListener listener) { RecentAdapter(@NonNull List<TopicSummary> topicSummaryList, BaseFragment.FragmentInteractionListener listener) {
this.context = context;
this.recentList = topicSummaryList; this.recentList = topicSummaryList;
mListener = (RecentFragment.RecentFragmentInteractionListener) listener; mListener = (RecentFragment.RecentFragmentInteractionListener) listener;
} }
@ -42,23 +42,29 @@ class RecentAdapter extends RecyclerView.Adapter<RecentAdapter.ViewHolder> {
@Override @Override
public void onBindViewHolder(final ViewHolder holder, final int position) { public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.mTitleView.setText(recentList.get(position).getSubject()); TopicSummary topicSummary = recentList.get(position);
holder.mDateTimeView.setText(recentList.get(position).getDateTimeModified()); holder.mTitleView.setText(topicSummary.getSubject());
holder.mUserView.setText(recentList.get(position).getLastUser()); if(BaseApplication.getInstance().isDisplayRelativeTimeEnabled()){
String timestamp = topicSummary.getLastPostTimestamp();
holder.topic = recentList.get(position); try{
holder.mDateTimeView.setReferenceTime(Long.valueOf(timestamp));
}
catch(NumberFormatException e){
Timber.e(e, "Invalid number format: %s", timestamp);
holder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
}
}
else
holder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
holder.mView.setOnClickListener(new View.OnClickListener() { holder.mUserView.setText(topicSummary.getLastUser());
@Override holder.topic = topicSummary;
public void onClick(View v) {
holder.mView.setOnClickListener(v -> {
if (null != mListener) { if (null != mListener) {
// Notify the active callbacks interface (the activity, if the // Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected. // fragment is attached to one) that an item has been selected.
mListener.onRecentFragmentInteraction(holder.topic); //? mListener.onRecentFragmentInteraction(holder.topic); //?
}
} }
}); });
} }
@ -72,7 +78,7 @@ class RecentAdapter extends RecyclerView.Adapter<RecentAdapter.ViewHolder> {
final View mView; final View mView;
final TextView mTitleView; final TextView mTitleView;
final TextView mUserView; final TextView mUserView;
final TextView mDateTimeView; final RelativeTimeTextView mDateTimeView;
public TopicSummary topic; public TopicSummary topic;
ViewHolder(View view) { ViewHolder(View view) {

28
app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java

@ -26,10 +26,10 @@ import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary; import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
@ -85,7 +85,6 @@ public class RecentFragment extends BaseFragment {
if (topicSummaries.isEmpty()) { if (topicSummaries.isEmpty()) {
recentTask = new RecentTask(this::onRecentTaskStarted, this::onRecentTaskFinished); recentTask = new RecentTask(this::onRecentTaskStarted, this::onRecentTaskFinished);
recentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.indexUrl.toString()); recentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.indexUrl.toString());
} }
Timber.d("onActivityCreated"); Timber.d("onActivityCreated");
} }
@ -100,7 +99,7 @@ public class RecentFragment extends BaseFragment {
// Set the adapter // Set the adapter
if (rootView instanceof RelativeLayout) { if (rootView instanceof RelativeLayout) {
progressBar = rootView.findViewById(R.id.progressBar); progressBar = rootView.findViewById(R.id.progressBar);
recentAdapter = new RecentAdapter(getActivity(), topicSummaries, fragmentInteractionListener); recentAdapter = new RecentAdapter(topicSummaries, fragmentInteractionListener);
CustomRecyclerView recyclerView = rootView.findViewById(R.id.list); CustomRecyclerView recyclerView = rootView.findViewById(R.id.list);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(recyclerView.getContext()); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(recyclerView.getContext());
@ -128,8 +127,13 @@ public class RecentFragment extends BaseFragment {
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (recentTask.isRunning()) if (recentTask!=null){
try{
if(recentTask.isRunning())
recentTask.cancel(true); recentTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
} }
@ -186,21 +190,11 @@ public class RecentFragment extends BaseFragment {
String dateTime = recent.get(i + 2).text(); String dateTime = recent.get(i + 2).text();
pattern = Pattern.compile("\\[(.*)]"); pattern = Pattern.compile("\\[(.*)]");
matcher = pattern.matcher(dateTime); matcher = pattern.matcher(dateTime);
if (matcher.find()) { if (matcher.find())
dateTime = matcher.group(1); fetchedRecent.add(new TopicSummary(link, title, lastUser, matcher.group(1)));
if (dateTime.contains(" am") || dateTime.contains(" pm") || else
dateTime.contains(" πμ") || dateTime.contains(" μμ")) {
dateTime = dateTime.replaceAll(":[0-5][0-9] ", " ");
} else {
dateTime = dateTime.substring(0, dateTime.lastIndexOf(":"));
}
if (!dateTime.contains(",")) {
dateTime = dateTime.replaceAll(".+? ([0-9])", "$1");
}
} else
throw new ParseException("Parsing failed (dateTime)"); throw new ParseException("Parsing failed (dateTime)");
fetchedRecent.add(new TopicSummary(link, title, lastUser, dateTime));
} }
return fetchedRecent; return fetchedRecent;
} }

101
app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadAdapter.java

@ -11,92 +11,60 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List; import java.util.List;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary; import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.views.RelativeTimeTextView;
import timber.log.Timber;
class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<TopicSummary> unreadList; private final List<TopicSummary> unreadList;
private final UnreadFragment.UnreadFragmentInteractionListener mListener; private final UnreadFragment.UnreadFragmentInteractionListener mListener;
private final MarkReadInteractionListener markReadListener;
private final int VIEW_TYPE_ITEM = 0;
private final int VIEW_TYPE_NADA = 1;
private final int VIEW_TYPE_MARK_READ = 2;
UnreadAdapter(@NonNull List<TopicSummary> topicSummaryList, UnreadAdapter(@NonNull List<TopicSummary> topicSummaryList,
BaseFragment.FragmentInteractionListener listener, BaseFragment.FragmentInteractionListener listener) {
MarkReadInteractionListener markReadInteractionListener) {
this.unreadList = topicSummaryList; this.unreadList = topicSummaryList;
mListener = (UnreadFragment.UnreadFragmentInteractionListener) listener; mListener = (UnreadFragment.UnreadFragmentInteractionListener) listener;
markReadListener = markReadInteractionListener;
}
@Override
public int getItemViewType(int position) {
if (unreadList.get(position).getDateTimeModified() == null) return VIEW_TYPE_MARK_READ;
return unreadList.get(position).getTopicUrl() == null ? VIEW_TYPE_NADA : VIEW_TYPE_ITEM;
} }
@NonNull @NonNull
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_ITEM) {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_unread_row, parent, false); .inflate(R.layout.fragment_unread_row, parent, false);
return new ViewHolder(view); return new ViewHolder(view);
} else if (viewType == VIEW_TYPE_NADA) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_unread_empty_row, parent, false);
return new EmptyViewHolder(view);
} else if (viewType == VIEW_TYPE_MARK_READ) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_unread_mark_read_row, parent, false);
return new MarkReadViewHolder(view);
}
return null;
} }
@Override @Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) { public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof UnreadAdapter.EmptyViewHolder) { TopicSummary topicSummary = unreadList.get(holder.getAdapterPosition());
final UnreadAdapter.EmptyViewHolder emptyViewHolder = (UnreadAdapter.EmptyViewHolder) holder;
emptyViewHolder.text.setText(unreadList.get(holder.getAdapterPosition()).getDateTimeModified());
} else if (holder instanceof UnreadAdapter.ViewHolder) {
final UnreadAdapter.ViewHolder viewHolder = (UnreadAdapter.ViewHolder) holder; final UnreadAdapter.ViewHolder viewHolder = (UnreadAdapter.ViewHolder) holder;
viewHolder.mTitleView.setText(unreadList.get(holder.getAdapterPosition()).getSubject()); viewHolder.mTitleView.setText(topicSummary.getSubject());
viewHolder.mDateTimeView.setText(unreadList.get(holder.getAdapterPosition()).getDateTimeModified()); if(BaseApplication.getInstance().isDisplayRelativeTimeEnabled()){
viewHolder.mUserView.setText(unreadList.get(position).getLastUser()); String timestamp = topicSummary.getLastPostTimestamp();
try{
viewHolder.topic = unreadList.get(holder.getAdapterPosition()); viewHolder.mDateTimeView.setReferenceTime(Long.valueOf(timestamp));
viewHolder.mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (null != mListener) {
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mListener.onUnreadFragmentInteraction(viewHolder.topic); //?
} }
catch(NumberFormatException e){
Timber.e(e, "Invalid number format: %s", timestamp);
viewHolder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
} }
}); }
} else if (holder instanceof UnreadAdapter.MarkReadViewHolder) { else
final UnreadAdapter.MarkReadViewHolder markReadViewHolder = (UnreadAdapter.MarkReadViewHolder) holder; viewHolder.mDateTimeView.setText(topicSummary.getLastPostSimplifiedDateTime());
markReadViewHolder.text.setText(unreadList.get(holder.getAdapterPosition()).getSubject());
markReadViewHolder.topic = unreadList.get(holder.getAdapterPosition());
markReadViewHolder.mView.setOnClickListener(new View.OnClickListener() { viewHolder.mUserView.setText(topicSummary.getLastUser());
@Override viewHolder.topic = topicSummary;
public void onClick(View v) {
viewHolder.mView.setOnClickListener(v -> {
if (null != mListener) { if (null != mListener) {
// Notify the active callbacks interface (the activity, if the // Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected. // fragment is attached to one) that an item has been selected.
markReadListener.onMarkReadInteraction(unreadList.get(holder.getAdapterPosition()).getTopicUrl()); mListener.onUnreadFragmentInteraction(viewHolder.topic); //?
}
} }
}); });
} }
}
@Override @Override
public int getItemCount() { public int getItemCount() {
@ -107,7 +75,7 @@ class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final View mView; final View mView;
final TextView mTitleView; final TextView mTitleView;
final TextView mUserView; final TextView mUserView;
final TextView mDateTimeView; final RelativeTimeTextView mDateTimeView;
public TopicSummary topic; public TopicSummary topic;
ViewHolder(View view) { ViewHolder(View view) {
@ -118,29 +86,4 @@ class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
mDateTimeView = view.findViewById(R.id.dateTime); mDateTimeView = view.findViewById(R.id.dateTime);
} }
} }
private static class EmptyViewHolder extends RecyclerView.ViewHolder {
final TextView text;
EmptyViewHolder(View view) {
super(view);
text = view.findViewById(R.id.text);
}
}
private static class MarkReadViewHolder extends RecyclerView.ViewHolder {
final View mView;
final TextView text;
public TopicSummary topic;
MarkReadViewHolder(View view) {
super(view);
mView = view;
text = view.findViewById(R.id.mark_read);
}
}
interface MarkReadInteractionListener {
void onMarkReadInteraction(String markReadLinkUrl);
}
} }

278
app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadFragment.java

@ -1,4 +1,3 @@
package gr.thmmy.mthmmy.activities.main.unread; package gr.thmmy.mthmmy.activities.main.unread;
import android.os.AsyncTask; import android.os.AsyncTask;
@ -7,32 +6,36 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary; import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CustomRecyclerView; import gr.thmmy.mthmmy.session.ValidateSessionTask;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
@ -50,14 +53,19 @@ public class UnreadFragment extends BaseFragment {
private MaterialProgressBar progressBar; private MaterialProgressBar progressBar;
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private FloatingActionButton markAsReadFAB;
private TextView noUnreadTopicsTextView;
private UnreadAdapter unreadAdapter; private UnreadAdapter unreadAdapter;
private List<TopicSummary> topicSummaries; private List<TopicSummary> topicSummaries;
private String markAsReadUrl;
private int numberOfPages = 0; private int numberOfPages = 0;
private int loadedPages = 0; private int loadedPages = 0;
private UnreadTask unreadTask; private UnreadTask unreadTask;
private MarkReadTask markReadTask; private MarkReadTask markReadTask;
private ValidateSessionTask validateSessionTask;
// Required empty public constructor // Required empty public constructor
public UnreadFragment() {} public UnreadFragment() {}
@ -81,18 +89,22 @@ public class UnreadFragment extends BaseFragment {
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
topicSummaries = new ArrayList<>(); topicSummaries = new ArrayList<>();
markAsReadUrl = BaseApplication.getInstance().getSessionManager().getMarkAllAsReadLink();
if(markAsReadUrl==null){
Timber.i("MarkAsRead URL is null.");
startValidateSessionTask();
}
} }
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
if (topicSummaries.isEmpty()) { if (topicSummaries.isEmpty()){
unreadTask = new UnreadTask(this::onUnreadTaskStarted, this::onUnreadTaskFinished); unreadTask = new UnreadTask(this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null; assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString()); unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
} }
markReadTask = new MarkReadTask(); markReadTask = new MarkReadTask(this::onMarkReadTaskStarted, this::onMarkReadTaskFinished);
Timber.d("onActivityCreated");
} }
@ -103,15 +115,21 @@ public class UnreadFragment extends BaseFragment {
final View rootView = inflater.inflate(R.layout.fragment_unread, container, false); final View rootView = inflater.inflate(R.layout.fragment_unread, container, false);
// Set the adapter // Set the adapter
if (rootView instanceof RelativeLayout) { if (rootView instanceof CoordinatorLayout) {
progressBar = rootView.findViewById(R.id.progressBar); progressBar = rootView.findViewById(R.id.progressBar);
unreadAdapter = new UnreadAdapter(topicSummaries, noUnreadTopicsTextView = rootView.findViewById(R.id.no_unread_topics);
fragmentInteractionListener, markReadLinkUrl -> { markAsReadFAB = rootView.findViewById(R.id.unread_fab);
if (!markReadTask.isRunning() && !unreadTask.isRunning()) {
markReadTask = new MarkReadTask(); if(topicSummaries.isEmpty()){
markReadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, markReadLinkUrl); hideMarkAsReadFAB();
noUnreadTopicsTextView.setVisibility(View.VISIBLE);
} }
}); else{
noUnreadTopicsTextView.setVisibility(View.INVISIBLE);
showMarkAsReadFAB();
}
unreadAdapter = new UnreadAdapter(topicSummaries, fragmentInteractionListener);
CustomRecyclerView recyclerView = rootView.findViewById(R.id.list); CustomRecyclerView recyclerView = rootView.findViewById(R.id.list);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(recyclerView.getContext()); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(recyclerView.getContext());
@ -125,15 +143,7 @@ public class UnreadFragment extends BaseFragment {
swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.primary); swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.primary);
swipeRefreshLayout.setColorSchemeResources(R.color.accent); swipeRefreshLayout.setColorSchemeResources(R.color.accent);
swipeRefreshLayout.setOnRefreshListener( swipeRefreshLayout.setOnRefreshListener(
() -> { this::startUnreadTask
if (!unreadTask.isRunning()) {
numberOfPages = 0;
loadedPages = 0;
unreadTask = new UnreadTask(this::onUnreadTaskStarted, this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
}
}
); );
} }
@ -143,46 +153,126 @@ public class UnreadFragment extends BaseFragment {
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (unreadTask!=null && unreadTask.isRunning()) cancelUnreadTaskIfRunning();
unreadTask.cancel(true); if (markReadTask!=null){
if (markReadTask!=null && markReadTask.isRunning()) try{
if(markReadTask.isRunning())
markReadTask.cancel(true); markReadTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
if (validateSessionTask!=null){
try{
if(validateSessionTask.isRunning())
validateSessionTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
if(topicSummaries!=null) if(topicSummaries!=null)
topicSummaries.clear(); topicSummaries.clear();
} }
private void startUnreadTask(){
if (unreadTask!=null) {
try{
if(!unreadTask.isRunning()){
numberOfPages = 0;
loadedPages = 0;
unreadTask = new UnreadTask(UnreadFragment.this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, UnreadFragment.this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
}
}
catch (NullPointerException ignored){ }
}
}
private void startValidateSessionTask(){
validateSessionTask = new ValidateSessionTask();
validateSessionTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void cancelUnreadTaskIfRunning(){
if (unreadTask!=null){
try{
if(unreadTask.isRunning())
unreadTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
}
public interface UnreadFragmentInteractionListener extends FragmentInteractionListener { public interface UnreadFragmentInteractionListener extends FragmentInteractionListener {
void onUnreadFragmentInteraction(TopicSummary topicSummary); void onUnreadFragmentInteraction(TopicSummary topicSummary);
} }
//---------------------------------------ASYNC TASK----------------------------------- private void showMarkAsReadFAB() {
markAsReadFAB.setOnClickListener(v -> showMarkAsReadConfirmationDialog());
markAsReadFAB.show();
markAsReadFAB.setTag(true);
}
private void hideMarkAsReadFAB() {
markAsReadFAB.setOnClickListener(null);
markAsReadFAB.hide();
markAsReadFAB.setTag(false);
}
private void showMarkAsReadConfirmationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle);
builder.setTitle("Mark all as read");
builder.setMessage("Are you sure that you want to mark ALL topics as read?");
builder.setPositiveButton("Yep", (dialogInterface, i) -> {
if (!markReadTask.isRunning() && markAsReadUrl!=null){
markReadTask = new MarkReadTask(this::onMarkReadTaskStarted, this::onMarkReadTaskFinished);
markReadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, markAsReadUrl);
}
});
builder.setNegativeButton("Nope", (dialogInterface, i) -> {});
builder.create().show();
}
private void hideProgressUI(){
progressBar.setVisibility(ProgressBar.INVISIBLE);
swipeRefreshLayout.setRefreshing(false);
}
//---------------------------------------UNREAD TASK-----------------------------------
private void onUnreadTaskStarted() { private void onUnreadTaskStarted() {
progressBar.setVisibility(ProgressBar.VISIBLE); progressBar.setVisibility(ProgressBar.VISIBLE);
} }
private void onUnreadTaskCancelled() {
swipeRefreshLayout.setRefreshing(false);
}
private void onUnreadTaskFinished(int resultCode, ArrayList<TopicSummary> fetchedUnread) { private void onUnreadTaskFinished(int resultCode, ArrayList<TopicSummary> fetchedUnread) {
if (resultCode == NetworkResultCodes.SUCCESSFUL) { if (resultCode == NetworkResultCodes.SUCCESSFUL) {
if(fetchedUnread!=null && !fetchedUnread.isEmpty()){ if(!fetchedUnread.isEmpty()){
if(loadedPages==0) if(loadedPages==0)
topicSummaries.clear(); topicSummaries.clear();
topicSummaries.addAll(fetchedUnread); topicSummaries.addAll(fetchedUnread);
unreadAdapter.notifyDataSetChanged(); noUnreadTopicsTextView.setVisibility(View.INVISIBLE);
showMarkAsReadFAB();
}
else {
topicSummaries.clear();
hideMarkAsReadFAB();
noUnreadTopicsTextView.setVisibility(View.VISIBLE);
} }
unreadAdapter.notifyDataSetChanged();
loadedPages++; loadedPages++;
if (loadedPages < numberOfPages) { if (loadedPages < numberOfPages) {
unreadTask = new UnreadTask(this::onUnreadTaskStarted, this::onUnreadTaskFinished); unreadTask = new UnreadTask(this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null; assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString() + ";start=" + loadedPages * 20); unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString() + ";start=" + loadedPages * 20);
} }
else { else
progressBar.setVisibility(ProgressBar.INVISIBLE); hideProgressUI();
swipeRefreshLayout.setRefreshing(false);
}
} }
else{ else{
progressBar.setVisibility(ProgressBar.INVISIBLE); hideProgressUI();
swipeRefreshLayout.setRefreshing(false);
if (resultCode == NetworkResultCodes.NETWORK_ERROR) if (resultCode == NetworkResultCodes.NETWORK_ERROR)
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
else else
@ -192,9 +282,8 @@ public class UnreadFragment extends BaseFragment {
} }
private class UnreadTask extends NewParseTask<ArrayList<TopicSummary>> { private class UnreadTask extends NewParseTask<ArrayList<TopicSummary>> {
UnreadTask(OnTaskStartedListener onTaskStartedListener, OnTaskCancelledListener onTaskCancelledListener, OnNetworkTaskFinishedListener<ArrayList<TopicSummary>> onParseTaskFinishedListener) {
UnreadTask(OnTaskStartedListener onTaskStartedListener, OnNetworkTaskFinishedListener<ArrayList<TopicSummary>> onParseTaskFinishedListener) { super(onTaskStartedListener, onTaskCancelledListener, onParseTaskFinishedListener);
super(onTaskStartedListener, onParseTaskFinishedListener);
} }
@Override @Override
@ -202,7 +291,6 @@ public class UnreadFragment extends BaseFragment {
Elements unread = document.select("table.bordercolor[cellspacing=1] tr:not(.titlebg)"); Elements unread = document.select("table.bordercolor[cellspacing=1] tr:not(.titlebg)");
ArrayList<TopicSummary> fetchedTopicSummaries = new ArrayList<>(); ArrayList<TopicSummary> fetchedTopicSummaries = new ArrayList<>();
if (!unread.isEmpty()) { if (!unread.isEmpty()) {
//topicSummaries.clear();
for (Element row : unread) { for (Element row : unread) {
Elements information = row.select("td"); Elements information = row.select("td");
String link = information.last().select("a").first().attr("href"); String link = information.last().select("a").first().attr("href");
@ -211,17 +299,9 @@ public class UnreadFragment extends BaseFragment {
Element lastUserAndDate = information.get(6); Element lastUserAndDate = information.get(6);
String lastUser = lastUserAndDate.select("a").text(); String lastUser = lastUserAndDate.select("a").text();
String dateTime = lastUserAndDate.select("span").html(); String dateTime = lastUserAndDate.select("span").html();
//dateTime = dateTime.replace("<br>", "");
dateTime = dateTime.substring(0, dateTime.indexOf("<br>")); dateTime = dateTime.substring(0, dateTime.indexOf("<br>"));
dateTime = dateTime.replace("<b>", ""); dateTime = dateTime.replace("<b>", "");
dateTime = dateTime.replace("</b>", ""); dateTime = dateTime.replace("</b>", "");
if (dateTime.contains(" am") || dateTime.contains(" pm") ||
dateTime.contains(" πμ") || dateTime.contains(" μμ"))
dateTime = dateTime.replaceAll(":[0-5][0-9] ", " ");
else
dateTime = dateTime.substring(0, dateTime.lastIndexOf(":"));
if (!dateTime.contains(","))
dateTime = dateTime.replaceAll(".+? ([0-9])", "$1");
fetchedTopicSummaries.add(new TopicSummary(link, title, lastUser, dateTime)); fetchedTopicSummaries.add(new TopicSummary(link, title, lastUser, dateTime));
} }
@ -230,7 +310,6 @@ public class UnreadFragment extends BaseFragment {
Element pagesElement = null, markRead = null; Element pagesElement = null, markRead = null;
if (topBar != null) { if (topBar != null) {
pagesElement = topBar.select("td.middletext").first(); pagesElement = topBar.select("td.middletext").first();
markRead = document.select("table:not(.bordercolor):not([width])").select("a") markRead = document.select("table:not(.bordercolor):not([width])").select("a")
.first(); .first();
} }
@ -243,79 +322,68 @@ public class UnreadFragment extends BaseFragment {
numberOfPages = 1; numberOfPages = 1;
} }
if (markRead != null && loadedPages == numberOfPages - 1) if (markRead != null && loadedPages == numberOfPages - 1){
fetchedTopicSummaries.add(new TopicSummary(markRead.attr("href"), markRead.text(), null, String retrievedMarkAsReadUrl = markRead.attr("href");
null)); if(!retrievedMarkAsReadUrl.equals(markAsReadUrl)) {
} else { markAsReadUrl = retrievedMarkAsReadUrl;
String message = document.select("table.bordercolor[cellspacing=1]").first().text(); BaseApplication.getInstance().getSessionManager().refreshSescFromUrl(retrievedMarkAsReadUrl);
if (message.contains("No messages")) { //It's english
message = "No unread posts!";
} else { //It's greek
message = "Δεν υπάρχουν μη αναγνωσμένα μηνύματα!";
} }
fetchedTopicSummaries.add(new TopicSummary(null, null, null, message));
} }
return fetchedTopicSummaries; return fetchedTopicSummaries;
} }
return new ArrayList<>();
}
@Override @Override
protected int getResultCode(Response response, ArrayList<TopicSummary> data) { protected int getResultCode(Response response, ArrayList<TopicSummary> topicSummaries) {
return NetworkResultCodes.SUCCESSFUL; return NetworkResultCodes.SUCCESSFUL;
} }
} }
private class MarkReadTask extends AsyncTask<String, Void, Integer> { //---------------------------------------MARKREAD TASK------------------------------------------
private static final int SUCCESS = 0; private void onMarkReadTaskStarted() {
private static final int NETWORK_ERROR = 1; cancelUnreadTaskIfRunning();
private static final int OTHER_ERROR = 2;
@Override
protected void onPreExecute() {
progressBar.setVisibility(ProgressBar.VISIBLE); progressBar.setVisibility(ProgressBar.VISIBLE);
} }
@Override private void onMarkReadTaskFinished(int resultCode, Boolean isSessionVerified) {
protected Integer doInBackground(String... strings) { hideProgressUI();
Request request = new Request.Builder() if (resultCode == NetworkResultCodes.SUCCESSFUL) {
.url(strings[0]) if (!isSessionVerified){
.build(); Toast.makeText(getContext(), "Session verification failed", Toast.LENGTH_SHORT).show();
try { startValidateSessionTask();
client.newCall(request).execute();
return SUCCESS;
} catch (IOException e) {
Timber.i(e, "IO Exception");
return NETWORK_ERROR;
} catch (Exception e) {
Timber.e(e, "Exception");
return OTHER_ERROR;
} }
else
startUnreadTask();
} }
else{
@Override hideProgressUI();
protected void onPostExecute(Integer result) { if (resultCode == NetworkResultCodes.NETWORK_ERROR)
progressBar.setVisibility(ProgressBar.GONE); Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
else
if (result == NETWORK_ERROR) { Toast.makeText(getContext(), "Unexpected error," +
Toast.makeText(getContext() " please contact the developers with the details", Toast.LENGTH_LONG).show();
, "Task was unsuccessful!\n Please check your internet conneciton.",
Toast.LENGTH_LONG).show();
} else if (result == OTHER_ERROR) {
Toast.makeText(getContext()
, "Fatal error!\n Task aborted...", Toast.LENGTH_LONG).show();
} else {
if (!unreadTask.isRunning()) {
numberOfPages = 0;
loadedPages = 0;
unreadTask = new UnreadTask(UnreadFragment.this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
} }
} }
private class MarkReadTask extends NewParseTask<Boolean> {
MarkReadTask(OnTaskStartedListener onTaskStartedListener, OnNetworkTaskFinishedListener<Boolean> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener);
} }
//TODO: Maybe extend this task and use isRunning() from ExternalAsyncTask instead (?) @Override
public boolean isRunning(){ protected Boolean parse(Document document, Response response) throws ParseException {
return getStatus() == AsyncTask.Status.RUNNING; Elements sessionVerificationFailed = document.select("td:containsOwn(Session " +
"verification failed. Please try logging out and back in again, and then try " +
"again.), td:containsOwn(Η επαλήθευση συνόδου απέτυχε. Παρακαλούμε κάντε " +
"αποσύνδεση, επανασύνδεση και ξαναδοκιμάστε.)");
return sessionVerificationFailed.isEmpty();
}
@Override
protected int getResultCode(Response response, Boolean isSessionVerified) {
return NetworkResultCodes.SUCCESSFUL;
} }
} }
} }

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

@ -19,15 +19,14 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.res.ResourcesCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import com.squareup.picasso.Picasso;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
@ -46,12 +45,11 @@ import gr.thmmy.mthmmy.activities.topic.TopicActivity;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.model.PostSummary; import gr.thmmy.mthmmy.model.PostSummary;
import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.utils.CenterVerticalSpan;
import gr.thmmy.mthmmy.utils.CircleTransform;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.Parcel; import gr.thmmy.mthmmy.utils.Parcel;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.ui.CenterVerticalSpan;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
@ -59,6 +57,7 @@ import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.emojiTagToHtml; import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.emojiTagToHtml;
import static gr.thmmy.mthmmy.utils.ui.PhotoViewUtils.displayPhotoViewImage;
/** /**
* Activity for user profile. When creating an Intent of this activity you need to bundle a <b>String</b> * Activity for user profile. When creating an Intent of this activity you need to bundle a <b>String</b>
@ -129,10 +128,10 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
avatarView = findViewById(R.id.user_thumbnail); avatarView = findViewById(R.id.user_thumbnail);
if (!Objects.equals(avatarUrl, "")) if (!Objects.equals(avatarUrl, ""))
//noinspection ConstantConditions loadAvatar(false);
loadAvatar();
else else
loadDefaultAvatar(); loadAvatar(true);
usernameView = findViewById(R.id.profile_activity_username); usernameView = findViewById(R.id.profile_activity_username);
usernameView.setTypeface(Typeface.createFromAsset(this.getAssets() usernameView.setTypeface(Typeface.createFromAsset(this.getAssets()
, "fonts/fontawesome-webfont.ttf")); , "fonts/fontawesome-webfont.ttf"));
@ -213,29 +212,21 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
if (pmFAB.getVisibility() != View.GONE) pmFAB.setEnabled(false); if (pmFAB.getVisibility() != View.GONE) pmFAB.setEnabled(false);
} }
private void loadAvatar(){ private void loadAvatar(Boolean loadDefault){
Picasso.with(this) String avatarUri;
.load(avatarUrl) if(loadDefault)
.fit() avatarUri = "R.drawable.ic_default_user_avatar";
.centerCrop() else {
.error(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources() avatarUri = avatarUrl;
, R.drawable.ic_default_user_avatar, null))) if(avatarUrl!=null)
.placeholder(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources() avatarView.setOnClickListener(v -> displayPhotoViewImage(ProfileActivity.this, avatarUrl));
, R.drawable.ic_default_user_avatar, null)))
.transform(new CircleTransform())
.into(avatarView);
} }
private void loadDefaultAvatar(){ Glide.with(this)
Picasso.with(this) .load(avatarUri)
.load(R.drawable.ic_default_user_avatar) .circleCrop()
.fit() .error(R.drawable.ic_default_user_avatar)
.centerCrop() .placeholder(R.drawable.ic_default_user_avatar)
.error(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources()
, R.drawable.ic_default_user_avatar, null)))
.placeholder(Objects.requireNonNull(ResourcesCompat.getDrawable(this.getResources()
, R.drawable.ic_default_user_avatar, null)))
.transform(new CircleTransform())
.into(avatarView); .into(avatarView);
} }
@ -318,11 +309,10 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
usernameView.setText(usernameSpan); usernameView.setText(usernameSpan);
} else if (usernameView.getText() != username) usernameView.setText(username); } else if (usernameView.getText() != username) usernameView.setText(username);
if (avatarUrl != null && !Objects.equals(avatarUrl, "")) if (avatarUrl != null && !Objects.equals(avatarUrl, ""))
//noinspection ConstantConditions loadAvatar(false);
loadAvatar();
else else
loadDefaultAvatar(); loadAvatar(true);
if (personalText != null) { if (personalText != null && !personalText.isEmpty()) {
personalTextView.setText(personalText); personalTextView.setText(personalText);
personalTextView.setVisibility(View.VISIBLE); personalTextView.setVisibility(View.VISIBLE);
} }

11
app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsAdapter.java

@ -1,10 +1,11 @@
package gr.thmmy.mthmmy.activities.profile.latestPosts; package gr.thmmy.mthmmy.activities.profile.latestPosts;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
@ -16,6 +17,7 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.PostSummary; import gr.thmmy.mthmmy.model.PostSummary;
import gr.thmmy.mthmmy.model.TopicSummary; import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.views.ReactiveWebView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
/** /**
@ -26,11 +28,13 @@ class LatestPostsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_EMPTY = -1; private static final int VIEW_TYPE_EMPTY = -1;
private static final int VIEW_TYPE_ITEM = 0; private static final int VIEW_TYPE_ITEM = 0;
private static final int VIEW_TYPE_LOADING = 1; private static final int VIEW_TYPE_LOADING = 1;
private final Context context;
private final LatestPostsFragment.LatestPostsFragmentInteractionListener interactionListener; private final LatestPostsFragment.LatestPostsFragmentInteractionListener interactionListener;
private final ArrayList<PostSummary> parsedTopicSummaries; private final ArrayList<PostSummary> parsedTopicSummaries;
LatestPostsAdapter(BaseFragment.FragmentInteractionListener interactionListener, LatestPostsAdapter(Context context, BaseFragment.FragmentInteractionListener interactionListener,
ArrayList<PostSummary> parsedTopicSummaries) { ArrayList<PostSummary> parsedTopicSummaries) {
this.context = context;
this.interactionListener = (LatestPostsFragment.LatestPostsFragmentInteractionListener) interactionListener; this.interactionListener = (LatestPostsFragment.LatestPostsFragmentInteractionListener) interactionListener;
this.parsedTopicSummaries = parsedTopicSummaries; this.parsedTopicSummaries = parsedTopicSummaries;
} }
@ -64,6 +68,7 @@ class LatestPostsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
return null; return null;
} }
@SuppressLint("ClickableViewAccessibility")
@Override @Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
if (holder instanceof LatestPostViewHolder) { if (holder instanceof LatestPostViewHolder) {
@ -99,7 +104,7 @@ class LatestPostsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final RelativeLayout latestPostsRow; final RelativeLayout latestPostsRow;
final TextView postTitle; final TextView postTitle;
final TextView postDate; final TextView postDate;
final WebView post; final ReactiveWebView post;
LatestPostViewHolder(View itemView) { LatestPostViewHolder(View itemView) {
super(itemView); super(itemView);

17
app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java

@ -6,7 +6,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@ -25,14 +24,13 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.PostSummary; import gr.thmmy.mthmmy.model.PostSummary;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.deobfuscateElements;
/** /**
* Use the {@link LatestPostsFragment#newInstance} factory method to create an instance of this fragment. * Use the {@link LatestPostsFragment#newInstance} factory method to create an instance of this fragment.
*/ */
@ -87,7 +85,7 @@ public class LatestPostsFragment extends BaseFragment implements LatestPostsAdap
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_latest_posts, container, false); final View rootView = inflater.inflate(R.layout.fragment_latest_posts, container, false);
latestPostsAdapter = new LatestPostsAdapter(fragmentInteractionListener, parsedTopicSummaries); latestPostsAdapter = new LatestPostsAdapter(this.getContext(), fragmentInteractionListener, parsedTopicSummaries);
RecyclerView mainContent = rootView.findViewById(R.id.profile_latest_posts_recycler); RecyclerView mainContent = rootView.findViewById(R.id.profile_latest_posts_recycler);
mainContent.setAdapter(latestPostsAdapter); mainContent.setAdapter(latestPostsAdapter);
final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
@ -176,13 +174,9 @@ public class LatestPostsFragment extends BaseFragment implements LatestPostsAdap
} }
protected void onPostExecute(Boolean result) { protected void onPostExecute(Boolean result) {
if (!result) { //Parse failed! if (Boolean.FALSE.equals(result))
Timber.d("Parse failed!"); Timber.e(new ParseException("Parsing failed(latest posts)"),"ParseException");
Toast.makeText(getContext()
, "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show();
getActivity().finish();
}
//Parse was successful
progressBar.setVisibility(ProgressBar.INVISIBLE); progressBar.setVisibility(ProgressBar.INVISIBLE);
latestPostsAdapter.notifyDataSetChanged(); latestPostsAdapter.notifyDataSetChanged();
isLoadingMore = false; isLoadingMore = false;
@ -208,7 +202,6 @@ public class LatestPostsFragment extends BaseFragment implements LatestPostsAdap
return true; return true;
} }
deobfuscateElements(latestPostsRows, false);
for (Element row : latestPostsRows) { for (Element row : latestPostsRows) {
String pTopicUrl, pTopicTitle, pDateTime, pPost; String pTopicUrl, pTopicTitle, pDateTime, pPost;
if (Integer.parseInt(row.attr("cellpadding")) == 4) { if (Integer.parseInt(row.attr("cellpadding")) == 4) {

3
app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java

@ -46,8 +46,6 @@ import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.deobfuscateElements;
public class StatsFragment extends Fragment { public class StatsFragment extends Fragment {
/** /**
* The key to use when putting profile's url String to {@link StatsFragment}'s Bundle. * The key to use when putting profile's url String to {@link StatsFragment}'s Bundle.
@ -173,7 +171,6 @@ public class StatsFragment extends Fragment {
return false; return false;
{ {
Elements titleRows = statsPage.select("table.bordercolor[align]>tbody>tr.titlebg"); Elements titleRows = statsPage.select("table.bordercolor[align]>tbody>tr.titlebg");
deobfuscateElements(titleRows, false);
generalStatisticsTitle = titleRows.first().text(); generalStatisticsTitle = titleRows.first().text();
if (userHasPosts) { if (userHasPosts) {
postingActivityByTimeTitle = titleRows.get(1).text(); postingActivityByTimeTitle = titleRows.get(1).text();

4
app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java

@ -27,9 +27,6 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.deobfuscateElements;
/** /**
* Use the {@link SummaryFragment#newInstance} factory method to create an instance of this fragment. * Use the {@link SummaryFragment#newInstance} factory method to create an instance of this fragment.
*/ */
@ -135,7 +132,6 @@ public class SummaryFragment extends Fragment {
//Contains all summary's rows //Contains all summary's rows
Elements summaryRows = profile.select(".bordercolor > tbody:nth-child(1) > tr:nth-child(2) tr"); Elements summaryRows = profile.select(".bordercolor > tbody:nth-child(1) > tr:nth-child(2) tr");
deobfuscateElements(summaryRows, false);
for (Element summaryRow : summaryRows) { for (Element summaryRow : summaryRows) {
String rowText = summaryRow.text(), pHtml = ""; String rowText = summaryRow.text(), pHtml = "";

1
app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsActivity.java

@ -9,6 +9,7 @@ import gr.thmmy.mthmmy.base.BaseActivity;
public class SettingsActivity extends BaseActivity { public class SettingsActivity extends BaseActivity {
public static final String DEFAULT_HOME_TAB = "pref_app_main_default_tab_key"; public static final String DEFAULT_HOME_TAB = "pref_app_main_default_tab_key";
public static final String DISPLAY_RELATIVE_TIME = "pref_app_display_relative_time_key";
public static final String NOTIFICATION_LED_KEY = "pref_notification_led_enable_key"; public static final String NOTIFICATION_LED_KEY = "pref_notification_led_enable_key";
public static final String NOTIFICATION_VIBRATION_KEY = "pref_notification_vibration_enable_key"; public static final String NOTIFICATION_VIBRATION_KEY = "pref_notification_vibration_enable_key";
public static final String POSTING_APP_SIGNATURE_ENABLE_KEY = "pref_posting_app_signature_enable_key"; public static final String POSTING_APP_SIGNATURE_ENABLE_KEY = "pref_posting_app_signature_enable_key";

21
app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsFragment.java

@ -42,6 +42,8 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
public static final String SELECTED_RINGTONE = "selectedRingtoneKey"; public static final String SELECTED_RINGTONE = "selectedRingtoneKey";
private static final String SILENT_SELECTED = "STFU"; private static final String SILENT_SELECTED = "STFU";
private static final String UNREAD = "Unread";
private SharedPreferences settingsFile; private SharedPreferences settingsFile;
private PREFS_TYPE prefs_type = PREFS_TYPE.NOT_SET; private PREFS_TYPE prefs_type = PREFS_TYPE.NOT_SET;
@ -65,7 +67,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
defaultHomeTabValues.add("1"); defaultHomeTabValues.add("1");
if(isLoggedIn = BaseApplication.getInstance().getSessionManager().isLoggedIn()){ if(isLoggedIn = BaseApplication.getInstance().getSessionManager().isLoggedIn()){
defaultHomeTabEntries.add("Unread"); defaultHomeTabEntries.add(UNREAD);
defaultHomeTabValues.add("2"); defaultHomeTabValues.add("2");
} }
} }
@ -171,16 +173,16 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
if(isLoggedIn&& prefs_type==PREFS_TYPE.GUEST) { if(isLoggedIn&& prefs_type==PREFS_TYPE.GUEST) {
prefs_type = PREFS_TYPE.USER; prefs_type = PREFS_TYPE.USER;
setPreferencesFromResource(R.xml.app_preferences_user, getPreferenceScreen().getKey()); setPreferencesFromResource(R.xml.app_preferences_user, getPreferenceScreen().getKey());
if(!defaultHomeTabEntries.contains("Unread")){ if(!defaultHomeTabEntries.contains(UNREAD)){
defaultHomeTabEntries.add("Unread"); defaultHomeTabEntries.add(UNREAD);
defaultHomeTabValues.add("2"); defaultHomeTabValues.add("2");
} }
} }
else if(!isLoggedIn&&prefs_type==PREFS_TYPE.USER){ else if(!isLoggedIn&&prefs_type==PREFS_TYPE.USER){
prefs_type = PREFS_TYPE.GUEST; prefs_type = PREFS_TYPE.GUEST;
setPreferencesFromResource(R.xml.app_preferences_guest,getPreferenceScreen().getKey()); setPreferencesFromResource(R.xml.app_preferences_guest,getPreferenceScreen().getKey());
if(defaultHomeTabEntries.contains("Unread")){ if(defaultHomeTabEntries.contains(UNREAD)){
defaultHomeTabEntries.remove("Unread"); defaultHomeTabEntries.remove(UNREAD);
defaultHomeTabValues.remove("2"); defaultHomeTabValues.remove("2");
} }
} }
@ -201,7 +203,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
BaseApplication.getInstance().startFirebaseCrashlyticsCollection(); BaseApplication.getInstance().startFirebaseCrashlyticsCollection();
else { else {
Timber.i("Crashlytics collection will be disabled after restarting."); Timber.i("Crashlytics collection will be disabled after restarting.");
Toast.makeText(BaseApplication.getInstance().getApplicationContext(), "This change will take effect once you restart the app.", Toast.LENGTH_SHORT).show(); displayRestartAppToTakeEffectToast();
} }
} else if (key.equals(getString(R.string.pref_privacy_analytics_enable_key))) { } else if (key.equals(getString(R.string.pref_privacy_analytics_enable_key))) {
enabled = sharedPreferences.getBoolean(key, false); enabled = sharedPreferences.getBoolean(key, false);
@ -210,6 +212,13 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
Timber.i("Analytics collection enabled."); Timber.i("Analytics collection enabled.");
else else
Timber.i("Analytics collection disabled."); Timber.i("Analytics collection disabled.");
} else if (key.equals(getString(R.string.pref_app_display_relative_time_key))
&& BaseApplication.getInstance().isDisplayRelativeTimeEnabled()!=sharedPreferences.getBoolean(key, false)){
displayRestartAppToTakeEffectToast();
}
} }
private void displayRestartAppToTakeEffectToast(){
Toast.makeText(BaseApplication.getInstance().getApplicationContext(), "This change will take effect once you restart the app.", Toast.LENGTH_SHORT).show();
} }
} }

5
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutAdapter.java

@ -23,7 +23,8 @@ import gr.thmmy.mthmmy.activities.profile.ProfileActivity;
import gr.thmmy.mthmmy.activities.topic.TopicActivity; import gr.thmmy.mthmmy.activities.topic.TopicActivity;
import gr.thmmy.mthmmy.model.Shout; import gr.thmmy.mthmmy.model.Shout;
import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.utils.CustomRecyclerView; import gr.thmmy.mthmmy.views.CustomRecyclerView;
import gr.thmmy.mthmmy.views.ReactiveWebView;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 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_TITLE;
@ -85,7 +86,7 @@ public class ShoutAdapter extends CustomRecyclerView.Adapter<ShoutAdapter.ShoutV
static class ShoutViewHolder extends CustomRecyclerView.ViewHolder { static class ShoutViewHolder extends CustomRecyclerView.ViewHolder {
TextView author, dateTime; TextView author, dateTime;
WebView shoutContent; ReactiveWebView shoutContent;
ShoutViewHolder(@NonNull View itemView) { ShoutViewHolder(@NonNull View itemView) {
super(itemView); super(itemView);

12
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxFragment.java

@ -19,13 +19,13 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.editorview.EditorView;
import gr.thmmy.mthmmy.editorview.EmojiKeyboard;
import gr.thmmy.mthmmy.model.Shout; import gr.thmmy.mthmmy.model.Shout;
import gr.thmmy.mthmmy.model.Shoutbox; import gr.thmmy.mthmmy.model.Shoutbox;
import gr.thmmy.mthmmy.utils.CustomRecyclerView;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.viewmodel.ShoutboxViewModel; import gr.thmmy.mthmmy.viewmodel.ShoutboxViewModel;
import gr.thmmy.mthmmy.views.CustomRecyclerView;
import gr.thmmy.mthmmy.views.editorview.EditorView;
import gr.thmmy.mthmmy.views.editorview.EmojiKeyboard;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber; import timber.log.Timber;
@ -105,9 +105,10 @@ public class ShoutboxFragment extends Fragment {
Timber.i("Shoutbox loaded successfully"); Timber.i("Shoutbox loaded successfully");
shoutAdapter.setShouts(shoutbox.getShouts()); shoutAdapter.setShouts(shoutbox.getShouts());
shoutAdapter.notifyDataSetChanged(); shoutAdapter.notifyDataSetChanged();
editorView.setVisibility(shoutbox.getShoutSend() == null ? View.GONE : View.VISIBLE);
} }
}); });
shoutboxViewModel.setOnShoutboxTaskStarted(this::onShoutboxTaskSarted); shoutboxViewModel.setOnShoutboxTaskStarted(this::onShoutboxTaskStarted);
shoutboxViewModel.setOnShoutboxTaskFinished(this::onShoutboxTaskFinished); shoutboxViewModel.setOnShoutboxTaskFinished(this::onShoutboxTaskFinished);
shoutboxViewModel.setOnSendShoutTaskStarted(this::onSendShoutTaskStarted); shoutboxViewModel.setOnSendShoutTaskStarted(this::onSendShoutTaskStarted);
shoutboxViewModel.setOnSendShoutTaskFinished(this::onSendShoutTaskFinished); shoutboxViewModel.setOnSendShoutTaskFinished(this::onSendShoutTaskFinished);
@ -115,9 +116,10 @@ public class ShoutboxFragment extends Fragment {
shoutboxViewModel.loadShoutbox(false); shoutboxViewModel.loadShoutbox(false);
} }
private void onShoutboxTaskSarted() { private void onShoutboxTaskStarted() {
Timber.i("Starting shoutbox task..."); Timber.i("Starting shoutbox task...");
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
editorView.setVisibility(View.GONE);
} }
private void onSendShoutTaskStarted() { private void onSendShoutTaskStarted() {

2
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxTask.java

@ -45,7 +45,7 @@ public class ShoutboxTask extends NewParseTask<Shoutbox> {
String formUrl = shoutboxForm.attr("action"); String formUrl = shoutboxForm.attr("action");
String sc = shoutboxForm.select("input[name=sc]").first().attr("value"); String sc = shoutboxForm.select("input[name=sc]").first().attr("value");
String shoutName = shoutboxForm.select("input[name=tp-shout-name]").first().attr("value"); String shoutName = shoutboxForm.select("input[name=tp-shout-name]").first().attr("value");
// TODO: make shout send nullable and disable shouting
Element shoutSendInput = shoutboxForm.select("input[name=shout_send]").first(); Element shoutSendInput = shoutboxForm.select("input[name=shout_send]").first();
String shoutSend = null; String shoutSend = null;
if (shoutSendInput != null) if (shoutSendInput != null)

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

@ -2,6 +2,8 @@ package gr.thmmy.mthmmy.activities.topic;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -47,16 +49,16 @@ import gr.thmmy.mthmmy.activities.topic.tasks.ReplyTask;
import gr.thmmy.mthmmy.activities.topic.tasks.TopicTask; import gr.thmmy.mthmmy.activities.topic.tasks.TopicTask;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.editorview.EmojiKeyboard;
import gr.thmmy.mthmmy.model.Bookmark; import gr.thmmy.mthmmy.model.Bookmark;
import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.model.TopicItem; import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.CustomLinearLayoutManager;
import gr.thmmy.mthmmy.utils.HTMLUtils; import gr.thmmy.mthmmy.utils.HTMLUtils;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import gr.thmmy.mthmmy.viewmodel.TopicViewModel;
import gr.thmmy.mthmmy.views.CustomLinearLayoutManager;
import gr.thmmy.mthmmy.views.editorview.EmojiKeyboard;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber; import timber.log.Timber;
@ -81,6 +83,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
public static final String BUNDLE_TOPIC_TITLE = "TOPIC_TITLE"; public static final String BUNDLE_TOPIC_TITLE = "TOPIC_TITLE";
private MaterialProgressBar progressBar; private MaterialProgressBar progressBar;
private TextView toolbarTitle; private TextView toolbarTitle;
private CustomLinearLayoutManager layoutManager;
private RecyclerView recyclerView; private RecyclerView recyclerView;
//Posts related //Posts related
private TopicAdapter topicAdapter; private TopicAdapter topicAdapter;
@ -123,6 +126,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
private Snackbar snackbar; private Snackbar snackbar;
private TopicViewModel viewModel; private TopicViewModel viewModel;
private EmojiKeyboard emojiKeyboard; private EmojiKeyboard emojiKeyboard;
private AlertDialog topicInfoDialog;
//Fix for vector drawables on android <21 //Fix for vector drawables on android <21
static { static {
@ -161,6 +165,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
toolbarTitle.setMarqueeRepeatLimit(-1); toolbarTitle.setMarqueeRepeatLimit(-1);
toolbarTitle.setText(topicTitle); toolbarTitle.setText(topicTitle);
toolbarTitle.setSelected(true); toolbarTitle.setSelected(true);
this.setToolbarOnLongClickListener(topicPageUrl);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -177,12 +182,13 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
recyclerView = findViewById(R.id.topic_recycler_view); recyclerView = findViewById(R.id.topic_recycler_view);
recyclerView.setHasFixedSize(true); recyclerView.setHasFixedSize(true);
//LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); //LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager( layoutManager = new CustomLinearLayoutManager(
getApplicationContext(), topicPageUrl); getApplicationContext(), topicPageUrl);
recyclerView.setLayoutManager(layoutManager); recyclerView.setLayoutManager(layoutManager);
topicAdapter = new TopicAdapter(this, emojiKeyboard, topicItems); topicAdapter = new TopicAdapter(this, emojiKeyboard, topicItems);
recyclerView.setAdapter(topicAdapter); recyclerView.setAdapter(topicAdapter);
recyclerView.setItemViewCacheSize(17); //Every page has maximum 15 posts + Poll + EditorView
replyFAB = findViewById(R.id.topic_fab); replyFAB = findViewById(R.id.topic_fab);
replyFAB.hide(); replyFAB.hide();
@ -253,8 +259,8 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
usersViewing.setText(HTMLUtils.getSpannableFromHtml(this, topicViewers)); usersViewing.setText(HTMLUtils.getSpannableFromHtml(this, topicViewers));
}); });
builder.setView(infoDialog); builder.setView(infoDialog);
AlertDialog dialog = builder.create(); topicInfoDialog = builder.create();
dialog.show(); topicInfoDialog.show();
return true; return true;
case R.id.menu_share: case R.id.menu_share:
Intent sendIntent = new Intent(android.content.Intent.ACTION_SEND); Intent sendIntent = new Intent(android.content.Intent.ACTION_SEND);
@ -312,6 +318,10 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
if(topicInfoDialog!=null){
topicInfoDialog.dismiss();
topicInfoDialog=null;
}
recyclerView.setAdapter(null); recyclerView.setAdapter(null);
viewModel.stopLoading(); viewModel.stopLoading();
} }
@ -653,11 +663,11 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
Toast.makeText(this, "Failed to remove vote", Toast.LENGTH_LONG).show(); 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 -> { viewModel.getPageIndicatorIndex().observe(this, pageIndicatorIndex -> {
if (pageIndicatorIndex == null) return; if (pageIndicatorIndex == null) return;
pageIndicator.setText(String.valueOf(pageIndicatorIndex) + "/" + pageIndicator.setText(pageIndicatorIndex + "/" +
String.valueOf(viewModel.getPageCount())); viewModel.getPageCount());
}); });
viewModel.getTopicTitle().observe(this, newTopicTitle -> { viewModel.getTopicTitle().observe(this, newTopicTitle -> {
if (newTopicTitle == null) return; if (newTopicTitle == null) return;
@ -682,11 +692,19 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
} }
}); });
viewModel.getTopicItems().observe(this, postList -> { viewModel.getTopicItems().observe(this, postList -> {
if (postList == null) progressBar.setVisibility(ProgressBar.VISIBLE); if (postList == null)
progressBar.setVisibility(ProgressBar.VISIBLE);
recyclerView.getRecycledViewPool().clear(); //Avoid inconsistency detected bug recyclerView.getRecycledViewPool().clear(); //Avoid inconsistency detected bug
topicItems.clear(); topicItems.clear();
/* A workaround to avoid automatic scrolling when a new page
page is loaded (it happens sometimes only)*/
recyclerView.setAdapter(topicAdapter);
if (postList != null) {
topicItems.addAll(postList); topicItems.addAll(postList);
topicAdapter.notifyDataSetChanged(); topicAdapter.notifyDataSetChanged();
}
}); });
/*viewModel.getFocusedPostIndex().observe(this, focusedPostIndex -> { /*viewModel.getFocusedPostIndex().observe(this, focusedPostIndex -> {
if (focusedPostIndex == null) return; if (focusedPostIndex == null) return;
@ -788,4 +806,31 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
} }
}); });
} }
/**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*/
void setToolbarOnLongClickListener(String url) {
toolbar.setOnLongClickListener(view -> {
//Try to set the clipboard text
try {
//Create a ClipboardManager
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(BUNDLE_TOPIC_URL, url));
//Make a toast to inform the user that the url was copied
Toast.makeText(
TopicActivity.this,
TopicActivity.this.getString(R.string.link_copied_msg),
Toast.LENGTH_SHORT).show();
}
//Something happened. Probably the device does not support this (report to Firebase)
catch (NullPointerException e) {
Timber.e(e, "Error while trying to copy topic's url.");
}
return true;
});
}
} }

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

@ -43,17 +43,16 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatButton; import androidx.appcompat.widget.AppCompatButton;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.github.mikephil.charting.charts.HorizontalBarChart; import com.github.mikephil.charting.charts.HorizontalBarChart;
import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry; import com.github.mikephil.charting.data.BarEntry;
import com.squareup.picasso.Picasso;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -65,17 +64,17 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.board.BoardActivity; import gr.thmmy.mthmmy.activities.board.BoardActivity;
import gr.thmmy.mthmmy.activities.profile.ProfileActivity; import gr.thmmy.mthmmy.activities.profile.ProfileActivity;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.editorview.EditorView;
import gr.thmmy.mthmmy.editorview.IEmojiKeyboard;
import gr.thmmy.mthmmy.model.Poll; import gr.thmmy.mthmmy.model.Poll;
import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.model.ThmmyFile;
import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.model.TopicItem; import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.CircleTransform;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import gr.thmmy.mthmmy.utils.parsing.ThmmyParser; import gr.thmmy.mthmmy.utils.parsing.ThmmyParser;
import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import gr.thmmy.mthmmy.viewmodel.TopicViewModel;
import gr.thmmy.mthmmy.views.ReactiveWebView;
import gr.thmmy.mthmmy.views.editorview.EditorView;
import gr.thmmy.mthmmy.views.editorview.IEmojiKeyboard;
import timber.log.Timber; import timber.log.Timber;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@ -364,7 +363,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
holder.post.setWebViewClient(new LinkLauncher()); holder.post.setWebViewClient(new LinkLauncher());
//noinspection ConstantConditions //noinspection ConstantConditions
loadAvatar(currentPost.getThumbnailURL(), holder.thumbnail); loadAvatar(currentPost.getThumbnailURL(), holder.thumbnail, holder.itemView.getContext());
//Sets username,submit date, index number, subject, post's and attached files texts //Sets username,submit date, index number, subject, post's and attached files texts
holder.username.setText(currentPost.getAuthor()); holder.username.setText(currentPost.getAuthor());
@ -645,7 +644,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
Post reply = (Post) topicItems.get(position); Post reply = (Post) topicItems.get(position);
//noinspection ConstantConditions //noinspection ConstantConditions
loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail); loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail, holder.itemView.getContext());
holder.username.setText(getSessionManager().getUsername()); holder.username.setText(getSessionManager().getUsername());
holder.itemView.setAlpha(1f); holder.itemView.setAlpha(1f);
@ -737,7 +736,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final EditMessageViewHolder holder = (EditMessageViewHolder) currentHolder; final EditMessageViewHolder holder = (EditMessageViewHolder) currentHolder;
//noinspection ConstantConditions //noinspection ConstantConditions
loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail); loadAvatar(getSessionManager().getAvatarLink(), holder.thumbnail, holder.itemView.getContext());
holder.username.setText(getSessionManager().getUsername()); holder.username.setText(getSessionManager().getUsername());
holder.editSubject.setText(currentPost.getSubject()); holder.editSubject.setText(currentPost.getSubject());
@ -807,16 +806,15 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} }
} }
private void loadAvatar(String imageUrl, ImageView imageView) { private void loadAvatar(String imageUrl, ImageView imageView, Context context) {
Picasso.with(context) if(imageUrl!=null)
imageUrl = imageUrl.trim();
Glide.with(context)
.load(imageUrl) .load(imageUrl)
.fit() .circleCrop()
.centerCrop() .error(R.drawable.ic_default_user_avatar_darker)
.error(Objects.requireNonNull(ResourcesCompat.getDrawable(context.getResources() .placeholder(R.drawable.ic_default_user_avatar_darker)
, 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(imageView); .into(imageView);
} }
@ -832,7 +830,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
final LinearLayout cardChildLinear; final LinearLayout cardChildLinear;
final TextView postDate, postNum, username, subject; final TextView postDate, postNum, username, subject;
final ImageView thumbnail; final ImageView thumbnail;
final public WebView post; final public ReactiveWebView post;
final ImageButton quoteToggle, overflowButton; final ImageButton quoteToggle, overflowButton;
final RelativeLayout header; final RelativeLayout header;
final LinearLayout userExtraInfo; final LinearLayout userExtraInfo;
@ -969,7 +967,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
if (viewModel.getCurrentPageIndex() == viewModel.getPageCount()) { if (viewModel.getCurrentPageIndex() == viewModel.getPageCount()) {
//same page //same page
postFocusListener.onPostFocusChange(getItemCount() - 1); postFocusListener.onPostFocusChange(getItemCount() - 1);
Timber.e("new"); Timber.d("new");
return true; return true;
} }
} }
@ -981,7 +979,7 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
for (int i = 0; i < topicItems.size(); i++) { for (int i = 0; i < topicItems.size(); i++) {
if (topicItems.get(i) instanceof Post && ((Post) topicItems.get(i)).getPostIndex() == testAgainst) { if (topicItems.get(i) instanceof Post && ((Post) topicItems.get(i)).getPostIndex() == testAgainst) {
//same page //same page
Timber.e(Integer.toString(i)); Timber.d(Integer.toString(i));
postFocusListener.onPostFocusChange(i); postFocusListener.onPostFocusChange(i);
return true; return true;
} }

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

@ -18,6 +18,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.Poll; import gr.thmmy.mthmmy.model.Poll;
import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.model.ThmmyFile;
@ -25,6 +26,8 @@ import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
/** /**
* Singleton used for parsing a topic. * Singleton used for parsing a topic.
@ -175,6 +178,7 @@ public class TopicParser {
p_specialRank, p_gender, p_personalText, p_numberOfPosts, p_postLastEditDate, p_specialRank, p_gender, p_personalText, p_numberOfPosts, p_postLastEditDate,
p_postURL, p_deletePostURL, p_editPostURL; p_postURL, p_deletePostURL, p_editPostURL;
int p_postNum, p_postIndex, p_numberOfStars, p_userColor; int p_postNum, p_postIndex, p_numberOfStars, p_userColor;
long p_timestamp;
boolean p_isDeleted = false, p_isUserMentionedInPost = false; boolean p_isDeleted = false, p_isUserMentionedInPost = false;
ArrayList<ThmmyFile> p_attachedFiles; ArrayList<ThmmyFile> p_attachedFiles;
@ -191,6 +195,7 @@ public class TopicParser {
p_postLastEditDate = null; p_postLastEditDate = null;
p_deletePostURL = null; p_deletePostURL = null;
p_editPostURL = null; p_editPostURL = null;
p_timestamp = 0;
//Language independent parsing //Language independent parsing
//Finds thumbnail url //Finds thumbnail url
@ -267,6 +272,12 @@ public class TopicParser {
p_postDate = p_postDate.substring(p_postDate.indexOf("στις:") + 6 p_postDate = p_postDate.substring(p_postDate.indexOf("στις:") + 6
, p_postDate.indexOf(" »")); , p_postDate.indexOf(" »"));
if (BaseApplication.getInstance().isDisplayRelativeTimeEnabled()) {
String timestamp = convertToTimestamp(p_postDate);
if(timestamp!=null)
p_timestamp = Long.valueOf(timestamp);
}
//Finds post's reply index number //Finds post's reply index number
Element postNum = thisRow.select("div.smalltext:matches(Απάντηση #)").first(); Element postNum = thisRow.select("div.smalltext:matches(Απάντηση #)").first();
if (postNum == null) { //Topic starter if (postNum == null) { //Topic starter

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

@ -2,6 +2,7 @@ package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask; import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.select.Selector; import org.jsoup.select.Selector;
@ -9,7 +10,6 @@ import org.jsoup.select.Selector;
import java.io.IOException; import java.io.IOException;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -46,7 +46,7 @@ public class PrepareForEditTask extends AsyncTask<String, Void, PrepareForEditRe
String postText, commitEditURL, numReplies, seqnum, sc, topic, icon; String postText, commitEditURL, numReplies, seqnum, sc, topic, icon;
OkHttpClient client = BaseApplication.getInstance().getClient(); OkHttpClient client = BaseApplication.getInstance().getClient();
Response response = client.newCall(request).execute(); Response response = client.newCall(request).execute();
document = ParseHelpers.parse(response.body().string()); document = Jsoup.parse(response.body().string());
Element form = document.select("form#postmodify").first(); Element form = document.select("form#postmodify").first();

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

@ -2,6 +2,7 @@ package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask; import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.parser.Parser; import org.jsoup.parser.Parser;
import org.jsoup.select.Selector; import org.jsoup.select.Selector;
@ -9,7 +10,6 @@ import org.jsoup.select.Selector;
import java.io.IOException; import java.io.IOException;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -43,7 +43,7 @@ public class PrepareForReplyTask extends AsyncTask<Integer, Void, PrepareForRepl
String numReplies, seqnum, sc, topic; String numReplies, seqnum, sc, topic;
try { try {
Response response = client.newCall(request).execute(); Response response = client.newCall(request).execute();
document = ParseHelpers.parse(response.body().string()); document = Jsoup.parse(response.body().string());
numReplies = replyPageUrl.substring(replyPageUrl.indexOf("num_replies=") + 12); numReplies = replyPageUrl.substring(replyPageUrl.indexOf("num_replies=") + 12);
seqnum = document.select("input[name=seqnum]").first().attr("value"); seqnum = document.select("input[name=seqnum]").first().attr("value");

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

@ -2,10 +2,14 @@ package gr.thmmy.mthmmy.activities.topic.tasks;
import android.os.AsyncTask; import android.os.AsyncTask;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import gr.thmmy.mthmmy.activities.topic.TopicParser; import gr.thmmy.mthmmy.activities.topic.TopicParser;
@ -41,18 +45,27 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
@Override @Override
protected TopicTaskResult doInBackground(String... strings) { protected TopicTaskResult doInBackground(String... strings) {
Document topic = null; Document topic = null;
String newPageUrl = strings[0]; String newPageUrl = strings[0];
//TODO: Perhaps decode all URLs app-wide (i.e. in BaseApplication)?
try {
//Decodes e.g. any %3B to ;
newPageUrl = URLDecoder.decode(newPageUrl, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
Timber.e(e, "Unsupported Encoding");
}
//Finds the index of message focus if present //Finds the index of message focus if present
int postFocus = 0; int postFocus = 0;
{
//TODO: Better parseInt handling - may rarely fail
if (newPageUrl.contains("msg")) { if (newPageUrl.contains("msg")) {
String tmp = newPageUrl.substring(newPageUrl.indexOf("msg") + 3); String tmp = newPageUrl.substring(newPageUrl.indexOf("msg") + 3);
if (tmp.contains(";")) if (tmp.contains(";"))
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(";"))); postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(';')));
else if (tmp.contains("#")) else if (tmp.contains("#"))
postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf("#"))); postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf('#')));
}
} }
Request request = new Request.Builder() Request request = new Request.Builder()
@ -60,7 +73,7 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
.build(); .build();
try { try {
Response response = BaseApplication.getInstance().getClient().newCall(request).execute(); Response response = BaseApplication.getInstance().getClient().newCall(request).execute();
topic = ParseHelpers.parse(response.body().string()); topic = Jsoup.parse(response.body().string());
ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic); ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic);
@ -120,10 +133,10 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
} }
private boolean isUnauthorized(Document document) { private boolean isUnauthorized(Document document) {
return document != null && document.select("body:contains(The topic or board you" + return document != null && !document.select("body:contains(The topic or board you" +
" are looking for appears to be either missing or off limits to you.)," + " are looking for appears to be either missing or off limits to you.)," +
"body:contains(Το θέμα ή πίνακας που ψάχνετε ή δεν υπάρχει ή δεν " + "body:contains(Το θέμα ή πίνακας που ψάχνετε ή δεν υπάρχει ή δεν " +
"είναι προσβάσιμο από εσάς.)").size() > 0; "είναι προσβάσιμο από εσάς.)").isEmpty();
} }
@Override @Override

2
app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadActivity.java

@ -64,11 +64,11 @@ import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.UploadCategory; import gr.thmmy.mthmmy.model.UploadCategory;
import gr.thmmy.mthmmy.model.UploadFile; import gr.thmmy.mthmmy.model.UploadFile;
import gr.thmmy.mthmmy.services.UploadsReceiver; import gr.thmmy.mthmmy.services.UploadsReceiver;
import gr.thmmy.mthmmy.utils.AppCompatSpinnerWithoutDefault;
import gr.thmmy.mthmmy.utils.FileUtils; import gr.thmmy.mthmmy.utils.FileUtils;
import gr.thmmy.mthmmy.utils.TakePhoto; import gr.thmmy.mthmmy.utils.TakePhoto;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseTask; import gr.thmmy.mthmmy.utils.parsing.ParseTask;
import gr.thmmy.mthmmy.views.AppCompatSpinnerWithoutDefault;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber; import timber.log.Timber;

6
app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadFieldsBuilderActivity.java

@ -156,11 +156,11 @@ public class UploadFieldsBuilderActivity extends BaseActivity {
private String buildTitle() { private String buildTitle() {
switch (typeRadio.getCheckedRadioButtonId()) { switch (typeRadio.getCheckedRadioButtonId()) {
case R.id.upload_fields_builder_radio_button_exams: case R.id.upload_fields_builder_radio_button_exams:
return "[" + courseMinifiedName + "] - " + "Θέματα εξετάσεων " + getPeriod() + " " + year.getText().toString(); return "[" + courseMinifiedName + "] " + "Θέματα εξετάσεων " + getPeriod() + " " + year.getText().toString();
case R.id.upload_fields_builder_radio_button_exam_solutions: case R.id.upload_fields_builder_radio_button_exam_solutions:
return "[" + courseMinifiedName + "] - " + "Λύσεις θεμάτων " + getPeriod() + " " + year.getText().toString(); return "[" + courseMinifiedName + "] " + "Λύσεις θεμάτων " + getPeriod() + " " + year.getText().toString();
case R.id.upload_fields_builder_radio_button_notes: case R.id.upload_fields_builder_radio_button_notes:
return "[" + courseMinifiedName + "] - " + "Σημειώσεις παραδόσεων " + year.getText().toString(); return "[" + courseMinifiedName + "] " + "Σημειώσεις παραδόσεων " + year.getText().toString();
default: default:
return null; return null;
} }

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

@ -45,10 +45,7 @@ import com.snatik.storage.Storage;
import net.gotev.uploadservice.UploadService; import net.gotev.uploadservice.UploadService;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
@ -67,6 +64,7 @@ import gr.thmmy.mthmmy.services.DownloadHelper;
import gr.thmmy.mthmmy.services.UploadsReceiver; import gr.thmmy.mthmmy.services.UploadsReceiver;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.FileUtils; import gr.thmmy.mthmmy.utils.FileUtils;
import gr.thmmy.mthmmy.utils.io.AssetUtils;
import gr.thmmy.mthmmy.viewmodel.BaseViewModel; import gr.thmmy.mthmmy.viewmodel.BaseViewModel;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -390,7 +388,7 @@ public abstract class BaseActivity extends AppCompatActivity {
DrawerBuilder drawerBuilder = new DrawerBuilder() DrawerBuilder drawerBuilder = new DrawerBuilder()
.withActivity(this) .withActivity(this)
.withToolbar(toolbar) .withToolbar(toolbar)
.withDrawerWidthDp((int) BaseApplication.getInstance().getDpWidth() / 2) .withDrawerWidthDp((int) BaseApplication.getInstance().getWidthInDp() / 2)
.withSliderBackgroundColor(ContextCompat.getColor(this, R.color.primary_light)) .withSliderBackgroundColor(ContextCompat.getColor(this, R.color.primary_light))
.withAccountHeader(accountHeader) .withAccountHeader(accountHeader)
.withOnDrawerItemClickListener((view, position, drawerItem) -> { .withOnDrawerItemClickListener((view, position, drawerItem) -> {
@ -428,7 +426,7 @@ public abstract class BaseActivity extends AppCompatActivity {
if (!sessionManager.isLoggedIn()) //When logged out or if user is guest if (!sessionManager.isLoggedIn()) //When logged out or if user is guest
startLoginActivity(); startLoginActivity();
else else
new LogoutTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); //Avoid delays between onPreExecute() and doInBackground() showLogoutDialog();
} else if (drawerItem.equals(ABOUT_ID)) { } else if (drawerItem.equals(ABOUT_ID)) {
if (!(BaseActivity.this instanceof AboutActivity)) { if (!(BaseActivity.this instanceof AboutActivity)) {
Intent intent = new Intent(BaseActivity.this, AboutActivity.class); Intent intent = new Intent(BaseActivity.this, AboutActivity.class);
@ -543,6 +541,17 @@ public abstract class BaseActivity extends AppCompatActivity {
//} //}
} }
} }
private void showLogoutDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle);
builder.setTitle("Logout");
builder.setMessage("Are you sure that you want to logout?");
builder.setPositiveButton("Yep", (dialogInterface, i) -> {
new LogoutTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); //Avoid delays between onPreExecute() and doInBackground()
});
builder.setNegativeButton("Nope", (dialogInterface, i) -> {});
builder.create().show();
}
//-----------------------------------------LOGOUT END----------------------------------------------- //-----------------------------------------LOGOUT END-----------------------------------------------
//---------------------------------------------BOOKMARKS-------------------------------------------- //---------------------------------------------BOOKMARKS--------------------------------------------
@ -557,24 +566,21 @@ public abstract class BaseActivity extends AppCompatActivity {
protected void setTopicBookmark(MenuItem thisPageBookmarkMenuButton) { protected void setTopicBookmark(MenuItem thisPageBookmarkMenuButton) {
this.thisPageBookmarkMenuButton = thisPageBookmarkMenuButton; this.thisPageBookmarkMenuButton = thisPageBookmarkMenuButton;
if (thisPageBookmark.matchExists(topicsBookmarked)) { if (thisPageBookmark.matchExists(topicsBookmarked))
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_true_accent_24dp); thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_true_accent_24dp);
} else { else
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_false_accent_24dp); thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_false_accent_24dp);
} }
}
protected void refreshTopicBookmark() { protected void refreshTopicBookmark() {
if (thisPageBookmarkMenuButton == null) { if (thisPageBookmarkMenuButton == null) return;
return;
}
loadSavedBookmarks(); loadSavedBookmarks();
if (thisPageBookmark.matchExists(topicsBookmarked)) { if (thisPageBookmark.matchExists(topicsBookmarked))
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_true_accent_24dp); thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_true_accent_24dp);
} else { else
thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_false_accent_24dp); thisPageBookmarkMenuButton.setIcon(R.drawable.ic_bookmark_false_accent_24dp);
} }
}
protected void topicMenuBookmarkClick() { protected void topicMenuBookmarkClick() {
if (thisPageBookmark.matchExists(topicsBookmarked)) { if (thisPageBookmark.matchExists(topicsBookmarked)) {
@ -729,7 +735,6 @@ public abstract class BaseActivity extends AppCompatActivity {
} }
} }
@Override @Override
public void onRequestPermissionsResult(int permsRequestCode, @NonNull String[] permissions public void onRequestPermissionsResult(int permsRequestCode, @NonNull String[] permissions
, @NonNull int[] grantResults) { , @NonNull int[] grantResults) {
@ -754,13 +759,7 @@ public abstract class BaseActivity extends AppCompatActivity {
} }
} }
//Uses temp file - called after permission grant private void prepareDownload(@NonNull ThmmyFile thmmyFile) {
private void downloadFile() {
if (checkPerms())
prepareDownload(tempThmmyFile);
}
private void prepareDownload(ThmmyFile thmmyFile) {
String fileName = thmmyFile.getFilename(); String fileName = thmmyFile.getFilename();
if (FileUtils.fileNameExists(fileName)) if (FileUtils.fileNameExists(fileName))
openDownloadPrompt(thmmyFile); openDownloadPrompt(thmmyFile);
@ -768,7 +767,7 @@ public abstract class BaseActivity extends AppCompatActivity {
DownloadHelper.enqueueDownload(thmmyFile); DownloadHelper.enqueueDownload(thmmyFile);
} }
private void openDownloadPrompt(final ThmmyFile thmmyFile) { private void openDownloadPrompt(@NonNull final ThmmyFile thmmyFile) {
View view = getLayoutInflater().inflate(R.layout.download_prompt_dialog, null); View view = getLayoutInflater().inflate(R.layout.download_prompt_dialog, null);
final BottomSheetDialog dialog = new BottomSheetDialog(this); final BottomSheetDialog dialog = new BottomSheetDialog(this);
dialog.setContentView(view); dialog.setContentView(view);
@ -832,32 +831,14 @@ public abstract class BaseActivity extends AppCompatActivity {
privacyPolicyTextView.setPadding(30, 20, 30, 20); privacyPolicyTextView.setPadding(30, 20, 30, 20);
privacyPolicyTextView.setTextColor(ContextCompat.getColor(this, R.color.primary_text)); privacyPolicyTextView.setTextColor(ContextCompat.getColor(this, R.color.primary_text));
SpannableConfiguration configuration = SpannableConfiguration.builder(this).linkResolver(new LinkResolverDef()).build(); SpannableConfiguration configuration = SpannableConfiguration.builder(this).linkResolver(new LinkResolverDef()).build();
StringBuilder stringBuilder = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(getAssets().open("PRIVACY.md")));
String line;
while ((line = reader.readLine()) != null) { String privacyPolicy = AssetUtils.readFileToText(BaseActivity.this,"PRIVACY.md");
stringBuilder.append(line); if(privacyPolicy!=null){
stringBuilder.append("\n"); Markwon.setMarkdown(privacyPolicyTextView, configuration, privacyPolicy);
}
Markwon.setMarkdown(privacyPolicyTextView, configuration, stringBuilder.toString());
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle); AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle);
builder.setView(privacyPolicyTextView); builder.setView(privacyPolicyTextView);
builder.setPositiveButton("Close", (dialogInterface, i) -> dialogInterface.dismiss()); builder.setPositiveButton("Close", (dialogInterface, i) -> dialogInterface.dismiss());
builder.show(); builder.show();
} catch (IOException e) {
Timber.e(e, "Error reading Privacy Policy from assets.");
} catch (Exception e) {
Timber.e(e, "Error in Privacy Policy dialog.");
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
Timber.e(e, "Error in Privacy Policy dialog (closing reader).");
}
} }
} }

50
app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java

@ -13,6 +13,7 @@ import android.widget.ImageView;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.bumptech.glide.Glide;
import com.crashlytics.android.Crashlytics; import com.crashlytics.android.Crashlytics;
import com.crashlytics.android.core.CrashlyticsCore; import com.crashlytics.android.core.CrashlyticsCore;
import com.franmontiel.persistentcookiejar.PersistentCookieJar; import com.franmontiel.persistentcookiejar.PersistentCookieJar;
@ -21,12 +22,10 @@ import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersisto
import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseApp;
import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.analytics.FirebaseAnalytics;
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor; import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor;
import com.jakewharton.picasso.OkHttp3Downloader;
import com.mikepenz.fontawesome_typeface_library.FontAwesome; import com.mikepenz.fontawesome_typeface_library.FontAwesome;
import com.mikepenz.iconics.IconicsDrawable; import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader; import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
import com.mikepenz.materialdrawer.util.DrawerImageLoader; import com.mikepenz.materialdrawer.util.DrawerImageLoader;
import com.squareup.picasso.Picasso;
import net.gotev.uploadservice.UploadService; import net.gotev.uploadservice.UploadService;
import net.gotev.uploadservice.okhttp.OkHttpStack; import net.gotev.uploadservice.okhttp.OkHttpStack;
@ -40,7 +39,7 @@ import java.util.concurrent.TimeUnit;
import gr.thmmy.mthmmy.BuildConfig; import gr.thmmy.mthmmy.BuildConfig;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.CrashReportingTree; import gr.thmmy.mthmmy.utils.crashreporting.CrashReportingTree;
import io.fabric.sdk.android.Fabric; import io.fabric.sdk.android.Fabric;
import okhttp3.CipherSuite; import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec; import okhttp3.ConnectionSpec;
@ -49,6 +48,8 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DISPLAY_RELATIVE_TIME;
public class BaseApplication extends Application { public class BaseApplication extends Application {
private static BaseApplication baseApplication; //BaseApplication singleton private static BaseApplication baseApplication; //BaseApplication singleton
@ -60,11 +61,15 @@ public class BaseApplication extends Application {
private OkHttpClient client; private OkHttpClient client;
private SessionManager sessionManager; private SessionManager sessionManager;
private boolean displayRelativeTime;
//TODO: maybe use PreferenceManager.getDefaultSharedPreferences here as well? //TODO: maybe use PreferenceManager.getDefaultSharedPreferences here as well?
private static final String SHARED_PREFS = "ThmmySharedPrefs"; private static final String SHARED_PREFS = "ThmmySharedPrefs";
//Display Metrics //Display Metrics
private static float dpWidth; private static float widthDp;
private static int widthPxl, heightPxl;
public static BaseApplication getInstance() { public static BaseApplication getInstance() {
return baseApplication; return baseApplication;
} }
@ -104,13 +109,12 @@ public class BaseApplication extends Application {
.addInterceptor(chain -> { .addInterceptor(chain -> {
Request request = chain.request(); Request request = chain.request();
HttpUrl oldUrl = chain.request().url(); HttpUrl oldUrl = chain.request().url();
if (Objects.equals(chain.request().url().host(), "www.thmmy.gr")) { if (Objects.equals(chain.request().url().host(), "www.thmmy.gr")
if (!oldUrl.toString().contains("theme=4")) { && !oldUrl.toString().contains("theme=4")) {
//Probably works but needs more testing: //Probably works but needs more testing:
HttpUrl newUrl = oldUrl.newBuilder().addQueryParameter("theme", "4").build(); HttpUrl newUrl = oldUrl.newBuilder().addQueryParameter("theme", "4").build();
request = request.newBuilder().url(newUrl).build(); request = request.newBuilder().url(newUrl).build();
} }
}
return chain.proceed(request); return chain.proceed(request);
}) })
.connectTimeout(40, TimeUnit.SECONDS) .connectTimeout(40, TimeUnit.SECONDS)
@ -137,11 +141,6 @@ public class BaseApplication extends Application {
client = builder.build(); client = builder.build();
sessionManager = new SessionManager(client, cookieJar, sharedPrefsCookiePersistor, sharedPrefs, draftsPrefs); sessionManager = new SessionManager(client, cookieJar, sharedPrefsCookiePersistor, sharedPrefs, draftsPrefs);
Picasso picasso = new Picasso.Builder(getApplicationContext())
.downloader(new OkHttp3Downloader(client))
.build();
Picasso.setSingletonInstance(picasso); //All following Picasso (with Picasso.with(Context context) requests will use this Picasso object
//Sets up upload service //Sets up upload service
UploadService.NAMESPACE = BuildConfig.APPLICATION_ID; UploadService.NAMESPACE = BuildConfig.APPLICATION_ID;
@ -151,12 +150,12 @@ public class BaseApplication extends Application {
DrawerImageLoader.init(new AbstractDrawerImageLoader() { DrawerImageLoader.init(new AbstractDrawerImageLoader() {
@Override @Override
public void set(ImageView imageView, Uri uri, Drawable placeholder, String tag) { public void set(ImageView imageView, Uri uri, Drawable placeholder, String tag) {
Picasso.with(imageView.getContext()).load(uri).placeholder(placeholder).into(imageView); Glide.with(imageView.getContext()).load(uri).circleCrop().error(placeholder).placeholder(placeholder).into(imageView);
} }
@Override @Override
public void cancel(ImageView imageView) { public void cancel(ImageView imageView) {
Picasso.with(imageView.getContext()).cancelRequest(imageView); Glide.with(imageView.getContext()).clear(imageView);
} }
@Override @Override
@ -172,7 +171,13 @@ public class BaseApplication extends Application {
}); });
DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics(); DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics();
dpWidth = displayMetrics.widthPixels / displayMetrics.density;
widthPxl = displayMetrics.widthPixels;
widthDp = widthPxl / displayMetrics.density;
heightPxl = displayMetrics.heightPixels;
displayRelativeTime = settingsSharedPrefs.getBoolean(DISPLAY_RELATIVE_TIME, true);
} }
//Getters //Getters
@ -188,10 +193,21 @@ public class BaseApplication extends Application {
return sessionManager; return sessionManager;
} }
public float getDpWidth() { public float getWidthInDp() {
return dpWidth; return widthDp;
}
public int getWidthInPixels() {
return widthPxl;
} }
public int getHeightInPixels() {
return heightPxl;
}
public boolean isDisplayRelativeTimeEnabled() {
return displayRelativeTime;
}
//--------------------Firebase-------------------- //--------------------Firebase--------------------

21
app/src/main/java/gr/thmmy/mthmmy/model/ThmmyFile.java

@ -1,5 +1,7 @@
package gr.thmmy.mthmmy.model; package gr.thmmy.mthmmy.model;
import android.webkit.URLUtil;
import java.net.URL; import java.net.URL;
public class ThmmyFile { public class ThmmyFile {
@ -8,27 +10,36 @@ public class ThmmyFile {
*/ */
private static final String TAG = "ThmmyFile"; private static final String TAG = "ThmmyFile";
private final URL fileUrl; private final URL fileUrl;
private final String filename, fileInfo; private final String fileName, fileInfo;
/** /**
* This constructor only creates a ThmmyFile object and <b>does not download</b> the file. * This constructor only creates a ThmmyFile object and <b>does not download</b> the file.
* *
* @param fileUrl {@link URL} object with file's url * @param fileUrl {@link URL} object with file's url
* @param filename {@link String} with desired file name * @param fileName {@link String} with desired file name
* @param fileInfo {@link String} with any extra information (like number of downloads) * @param fileInfo {@link String} with any extra information (like number of downloads)
*/ */
public ThmmyFile(URL fileUrl, String filename, String fileInfo) { public ThmmyFile(URL fileUrl, String fileName, String fileInfo) {
this.fileUrl = fileUrl; this.fileUrl = fileUrl;
this.filename = filename; if(fileName!=null)
this.fileName = fileName;
else
this.fileName = URLUtil.guessFileName(fileUrl.toString(), null, null);
this.fileInfo = fileInfo; this.fileInfo = fileInfo;
} }
public ThmmyFile(URL fileUrl) {
this.fileUrl = fileUrl;
this.fileName = URLUtil.guessFileName(fileUrl.toString(), null, null);
this.fileInfo = null;
}
public URL getFileUrl() { public URL getFileUrl() {
return fileUrl; return fileUrl;
} }
public String getFilename() { public String getFilename() {
return filename; return fileName;
} }
public String getFileInfo() { public String getFileInfo() {

39
app/src/main/java/gr/thmmy/mthmmy/model/Topic.java

@ -8,7 +8,7 @@ package gr.thmmy.mthmmy.model;
* not, whether it's sticky or not and whether it contains an unread post or not.</b>. * not, whether it's sticky or not and whether it contains an unread post or not.</b>.
*/ */
public class Topic extends TopicSummary { public class Topic extends TopicSummary {
private final String lastPostUrl, stats; private final String lastPostUrl, starter, stats;
private final boolean locked, sticky, unread; private final boolean locked, sticky, unread;
// Suppresses default constructor // Suppresses default constructor
@ -16,6 +16,7 @@ public class Topic extends TopicSummary {
private Topic() { private Topic() {
super(); super();
this.lastPostUrl = null; this.lastPostUrl = null;
this.starter = null;
this.stats = null; this.stats = null;
this.locked = false; this.locked = false;
this.sticky = false; this.sticky = false;
@ -29,57 +30,31 @@ public class Topic extends TopicSummary {
* @param topicUrl this topic's url * @param topicUrl this topic's url
* @param subject this topic's subject * @param subject this topic's subject
* @param starter this topic starter's username * @param starter this topic starter's username
* @param lastPost username of topic's last post's author * @param lastUser username of topic's last post's author
* @param lastPostUrl url of topic's last post * @param lastPostUrl url of topic's last post
* @param stats this topic's view and reply stats * @param stats this topic's view and reply stats
* @param locked whether this topic is locked or not * @param locked whether this topic is locked or not
* @param sticky whether this topic is sticky or not * @param sticky whether this topic is sticky or not
* @param unread whether this topic contains an unread post or not * @param unread whether this topic contains an unread post or not
*/ */
public Topic(String topicUrl, String subject, String starter, String lastPost, String lastPostUrl, public Topic(String topicUrl, String subject, String starter, String lastUser, String LastPostDateTime, String lastPostUrl,
String stats, boolean locked, boolean sticky, boolean unread) { String stats, boolean locked, boolean sticky, boolean unread) {
super(topicUrl, subject, starter, lastPost); super(topicUrl, subject, lastUser, LastPostDateTime);
this.lastPostUrl = lastPostUrl; this.lastPostUrl = lastPostUrl;
this.starter = starter;
this.stats = stats; this.stats = stats;
this.locked = locked; this.locked = locked;
this.sticky = sticky; this.sticky = sticky;
this.unread = unread; this.unread = unread;
} }
/**
* Gets this topic's url.
*
* @return this topic's url
*/
public String getUrl() {
return topicUrl;
}
/**
* Gets this topic's subject.
*
* @return this topic's subject
*/
public String getSubject() {
return subject;
}
/** /**
* Gets this topic's starter username. * Gets this topic's starter username.
* *
* @return this topic's starter username * @return this topic's starter username
*/ */
public String getStarter() { public String getStarter() {
return lastUser; return starter;
}
/**
* Gets this topic's last post's date and time.
*
* @return last post's date and time
*/
public String getLastPostDateAndTime() {
return dateTimeModified;
} }
/** /**

55
app/src/main/java/gr/thmmy/mthmmy/model/TopicSummary.java

@ -1,5 +1,8 @@
package gr.thmmy.mthmmy.model; package gr.thmmy.mthmmy.model;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.simplifyDateTime;
/** /**
* Class that defines the summary of a topic. All member variables are declared final (thus no * Class that defines the summary of a topic. All member variables are declared final (thus no
* setters are supplied). Class has one constructor and getter methods for all variables. * setters are supplied). Class has one constructor and getter methods for all variables.
@ -7,10 +10,12 @@ package gr.thmmy.mthmmy.model;
* time of this topic's last post.</b>. * time of this topic's last post.</b>.
*/ */
public class TopicSummary { public class TopicSummary {
final String topicUrl; private final String topicUrl;
final String subject; private final String subject;
final String lastUser; private final String lastUser;
final String dateTimeModified; private final String lastPostDateTime;
private final String lastPostSimplifiedDateTime;
private final String lastPostTimestamp;
// Suppresses default constructor // Suppresses default constructor
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -18,23 +23,27 @@ public class TopicSummary {
this.topicUrl = null; this.topicUrl = null;
this.subject = null; this.subject = null;
this.lastUser = null; this.lastUser = null;
this.dateTimeModified = null; this.lastPostDateTime = null;
this.lastPostSimplifiedDateTime = null;
this.lastPostTimestamp = null;
} }
/** /**
* Constructor specifying all class variables necessary to summarise this topic. All variables * Constructor specifying all class variables necessary to summarize this topic. All variables
* are declared final, once assigned they can not change. * are declared final, once assigned they can not change.
* *
* @param topicUrl this topic's url * @param topicUrl this topic's url
* @param subject this topic's subject * @param subject this topic's subject
* @param lastUser username of this topic's last author * @param lastUser username of this topic's last post's author
* @param dateTimeModified this topic's date and time of last post * @param lastPostDateTime this topic's date and time of last post
*/ */
public TopicSummary(String topicUrl, String subject, String lastUser, String dateTimeModified) { public TopicSummary(String topicUrl, String subject, String lastUser, String lastPostDateTime) {
this.topicUrl = topicUrl; this.topicUrl = topicUrl;
this.subject = subject; this.subject = subject;
this.lastUser = lastUser; this.lastUser = lastUser;
this.dateTimeModified = dateTimeModified; this.lastPostDateTime = lastPostDateTime;
this.lastPostTimestamp = convertToTimestamp(lastPostDateTime);
this.lastPostSimplifiedDateTime = simplifyDateTime(lastPostDateTime);
} }
/** /**
@ -56,9 +65,9 @@ public class TopicSummary {
} }
/** /**
* Gets username of this topic's last author. * Gets username of this topic's last post's author.
* *
* @return username of last author * @return username of last post's author
*/ */
public String getLastUser() { public String getLastUser() {
return lastUser; return lastUser;
@ -69,7 +78,25 @@ public class TopicSummary {
* *
* @return this topic's date and time of last post * @return this topic's date and time of last post
*/ */
public String getDateTimeModified() { public String getLastPostDateTime() {
return dateTimeModified; return lastPostDateTime;
}
/**
* Gets this topic's simplified date and time of last post.
*
* @return this topic's simplified date and time of last post
*/
public String getLastPostSimplifiedDateTime() {
return lastPostSimplifiedDateTime;
}
/**
* Gets the timestamp of this topic's last post.
*
* @return the timestamp of this topic's last post
*/
public String getLastPostTimestamp() {
return lastPostTimestamp;
} }
} }

5
app/src/main/java/gr/thmmy/mthmmy/services/DownloadHelper.java

@ -6,6 +6,8 @@ import android.net.Uri;
import android.os.Environment; import android.os.Environment;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import java.io.File; import java.io.File;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
@ -34,6 +36,7 @@ public class DownloadHelper {
DownloadManager.Request request = new DownloadManager.Request(downloadURI); DownloadManager.Request request = new DownloadManager.Request(downloadURI);
Cookie thmmyCookie = BaseApplication.getInstance().getSessionManager().getThmmyCookie(); Cookie thmmyCookie = BaseApplication.getInstance().getSessionManager().getThmmyCookie();
if(thmmyCookie!=null)
request.addRequestHeader("Cookie", thmmyCookie.name() + "=" + thmmyCookie.value()); request.addRequestHeader("Cookie", thmmyCookie.name() + "=" + thmmyCookie.value());
request.setTitle(fileName); request.setTitle(fileName);
request.setMimeType(getMimeType(fileName)); request.setMimeType(getMimeType(fileName));
@ -49,6 +52,7 @@ public class DownloadHelper {
} }
} }
@NonNull
private static String renameFileIfExists(String originalFileName) { private static String renameFileIfExists(String originalFileName) {
final String dirPath = SAVE_DIR.getAbsolutePath(); final String dirPath = SAVE_DIR.getAbsolutePath();
File file = new File(dirPath, originalFileName); File file = new File(dirPath, originalFileName);
@ -68,7 +72,6 @@ public class DownloadHelper {
file = new File(dirPath, String.format(nameFormat, i)); file = new File(dirPath, String.format(nameFormat, i));
} }
return file.getName(); return file.getName();
} }
} }

113
app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java

@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import com.franmontiel.persistentcookiejar.PersistentCookieJar; import com.franmontiel.persistentcookiejar.PersistentCookieJar;
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor; import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
@ -18,7 +19,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.Cookie; import okhttp3.Cookie;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
@ -39,6 +39,8 @@ public class SessionManager {
private static final HttpUrl loginUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=login2"); private static final HttpUrl loginUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=login2");
public static final HttpUrl unreadUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=unread;all;start=0;theme=4"); public static final HttpUrl unreadUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=unread;all;start=0;theme=4");
public static final HttpUrl shoutboxUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=tpmod;sa=shoutbox;theme=4"); public static final HttpUrl shoutboxUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=tpmod;sa=shoutbox;theme=4");
private static final String baseLogoutLink = "https://www.thmmy.gr/smf/index.php?action=logout;sesc=";
private static final String baseMarkAllAsReadLink = "https://www.thmmy.gr/smf/index.php?action=markasread;sa=all;sesc=";
private static final String guestName = "Guest"; private static final String guestName = "Guest";
//Response Codes //Response Codes
@ -63,7 +65,9 @@ public class SessionManager {
private static final String USER_ID = "UserID"; private static final String USER_ID = "UserID";
private static final String AVATAR_LINK = "AvatarLink"; private static final String AVATAR_LINK = "AvatarLink";
private static final String HAS_AVATAR = "HasAvatar"; private static final String HAS_AVATAR = "HasAvatar";
private static final String SESC = "Sesc";
private static final String LOGOUT_LINK = "LogoutLink"; private static final String LOGOUT_LINK = "LogoutLink";
private static final String MARK_ALL_AS_READ_LINK = "MarkAllAsReadLink";
private static final String LOGGED_IN = "LoggedIn"; private static final String LOGGED_IN = "LoggedIn";
private static final String LOGIN_SCREEN_AS_DEFAULT = "LoginScreenAsDefault"; private static final String LOGIN_SCREEN_AS_DEFAULT = "LoginScreenAsDefault";
@ -84,7 +88,7 @@ public class SessionManager {
* Always call it in a separate thread. * Always call it in a separate thread.
*/ */
public int login(String... strings) { public int login(String... strings) {
Timber.i("Logging in..."); Timber.d("Logging in...");
//Build the login request for each case //Build the login request for each case
Request request; Request request;
@ -112,10 +116,9 @@ public class SessionManager {
try { try {
//Make request & handle response //Make request & handle response
Response response = client.newCall(request).execute(); Response response = client.newCall(request).execute();
Document document = ParseHelpers.parse(response.body().string()); Document document = Jsoup.parse(response.body().string());
if (validateRetrievedCookies()) if (validateRetrievedCookies()) {
{
Timber.i("Login successful!"); Timber.i("Login successful!");
setPersistentCookieSession(); //Store cookies setPersistentCookieSession(); //Store cookies
@ -129,7 +132,10 @@ public class SessionManager {
if (avatar != null) if (avatar != null)
editor.putString(AVATAR_LINK, avatar); editor.putString(AVATAR_LINK, avatar);
editor.putBoolean(HAS_AVATAR, avatar != null); editor.putBoolean(HAS_AVATAR, avatar != null);
editor.putString(LOGOUT_LINK, extractLogoutLink(document)); String sesc = extractSesc(document);
editor.putString(SESC, sesc);
editor.putString(LOGOUT_LINK, generateLogoutLink(sesc));
editor.putString(MARK_ALL_AS_READ_LINK, generateMarkAllAsReadLink(sesc));
editor.apply(); editor.apply();
return SUCCESS; return SUCCESS;
@ -181,10 +187,10 @@ public class SessionManager {
* Always call it in a separate thread in a way that won't hinder performance (e.g. after * Always call it in a separate thread in a way that won't hinder performance (e.g. after
* fragments' data are retrieved). * fragments' data are retrieved).
*/ */
public void validateSession() { void validateSession() {
Timber.i("Validating session..."); Timber.i("Validating session...");
if (isLoggedIn()) { if (isLoggedIn()) {
Timber.i("Refreshing session...");
int loginResult = login(); int loginResult = login();
if (loginResult != FAILURE) if (loginResult != FAILURE)
return; return;
@ -204,21 +210,18 @@ public class SessionManager {
setLoginScreenAsDefault(false); setLoginScreenAsDefault(false);
} }
/** /**
* Logout function. Always call it in a separate thread. * Logout function. Always call it in a separate thread.
*/ */
public int logout() { public int logout() {
Timber.i("Logging out..."); Timber.i("Logging out...");
try {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(sharedPrefs.getString(LOGOUT_LINK, "LogoutLink")) .url(getLogoutLink())
.build(); .build();
try {
//Make request & handle response //Make request & handle response
Response response = client.newCall(request).execute(); Response response = client.newCall(request).execute();
Document document = ParseHelpers.parse(response.body().string()); Document document = Jsoup.parse(response.body().string());
Elements loginButton = document.select("[value=Login]"); //Attempt to find login button Elements loginButton = document.select("[value=Login]"); //Attempt to find login button
if (!loginButton.isEmpty()) //If login button exists, logout was successful if (!loginButton.isEmpty()) //If login button exists, logout was successful
@ -241,6 +244,15 @@ public class SessionManager {
guestLogin(); guestLogin();
} }
} }
public void refreshSescFromUrl(String url){
String sesc = extractSescFromLink(url);
if(sesc!=null){
setSesc(sesc);
setLogoutLink(generateLogoutLink(sesc));
setMarkAsReadLink(sesc);
}
}
//--------------------------------------AUTH ENDS----------------------------------------------- //--------------------------------------AUTH ENDS-----------------------------------------------
//---------------------------------------GETTERS------------------------------------------------ //---------------------------------------GETTERS------------------------------------------------
@ -258,14 +270,31 @@ public class SessionManager {
public Cookie getThmmyCookie() { public Cookie getThmmyCookie() {
List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl); List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl);
for(Cookie cookie: cookieList) for(Cookie cookie: cookieList) {
{
if(cookie.name().equals("THMMYgrC00ki3")) if(cookie.name().equals("THMMYgrC00ki3"))
return cookie; return cookie;
} }
return null; return null;
} }
public String getMarkAllAsReadLink() {
String markAsReadLink = sharedPrefs.getString(MARK_ALL_AS_READ_LINK, null);
if(markAsReadLink == null){ //For older versions, extract it from logout link (otherwise user would have to login again)
String sesc = extractSescFromLink(getLogoutLink());
if(sesc!=null) {
setSesc(sesc);
markAsReadLink = generateMarkAllAsReadLink(sesc);
setMarkAsReadLink(markAsReadLink);
return markAsReadLink;
}
}
return markAsReadLink; // Warning: it can be null
}
private String getLogoutLink() {
return sharedPrefs.getString(LOGOUT_LINK, null);
}
public boolean hasAvatar() { public boolean hasAvatar() {
return sharedPrefs.getBoolean(HAS_AVATAR, false); return sharedPrefs.getBoolean(HAS_AVATAR, false);
} }
@ -280,6 +309,27 @@ public class SessionManager {
//--------------------------------------GETTERS END--------------------------------------------- //--------------------------------------GETTERS END---------------------------------------------
//---------------------------------------SETTERS------------------------------------------------
private void setSesc(String sesc){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(SESC, sesc);
editor.apply();
}
private void setMarkAsReadLink(String markAllAsReadLink){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(MARK_ALL_AS_READ_LINK, markAllAsReadLink);
editor.apply();
}
private void setLogoutLink(String logoutLink){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(LOGOUT_LINK, logoutLink);
editor.apply();
}
//--------------------------------------SETTERS END---------------------------------------------
//------------------------------------OTHER FUNCTIONS------------------------------------------- //------------------------------------OTHER FUNCTIONS-------------------------------------------
private boolean validateRetrievedCookies() { private boolean validateRetrievedCookies() {
List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl); List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl);
@ -353,7 +403,6 @@ public class SessionManager {
return "User"; //return a default username return "User"; //return a default username
} }
@NonNull
private int extractUserId(@NonNull Document doc) { private int extractUserId(@NonNull Document doc) {
try{ try{
Elements elements = doc.select("a:containsOwn(Εμφάνιση των μηνυμάτων σας), a:containsOwn(Show own posts)"); Elements elements = doc.select("a:containsOwn(Εμφάνιση των μηνυμάτων σας), a:containsOwn(Show own posts)");
@ -383,17 +432,33 @@ public class SessionManager {
return null; return null;
} }
@NonNull private String extractSesc(@NonNull Document doc) {
private String extractLogoutLink(@NonNull Document doc) {
Elements logoutLink = doc.select("a[href^=https://www.thmmy.gr/smf/index.php?action=logout;sesc=]"); Elements logoutLink = doc.select("a[href^=https://www.thmmy.gr/smf/index.php?action=logout;sesc=]");
if (!logoutLink.isEmpty()) { if (!logoutLink.isEmpty()) {
String link = logoutLink.first().attr("href"); String link = logoutLink.first().attr("href");
if (link != null && !link.isEmpty()) return extractSescFromLink(link);
return link;
} }
Timber.e(new ParseException("Parsing failed(logoutLink extraction)"),"ParseException"); Timber.e(new ParseException("Parsing failed(extractSesc)"),"ParseException");
return "https://www.thmmy.gr/smf/index.php?action=logout"; //return a default link return null;
}
private String extractSescFromLink(String link){
if (link != null){
Pattern pattern = Pattern.compile(".+;sesc=(\\w+)");
Matcher matcher = pattern.matcher(link);
if (matcher.find())
return matcher.group(1);
}
Timber.e(new ParseException("Parsing failed(extractSescFromLogoutLink)"),"ParseException");
return null;
}
private String generateLogoutLink(String sesc){
return baseLogoutLink + sesc;
}
private String generateMarkAllAsReadLink(String sesc){
return baseMarkAllAsReadLink + sesc;
} }
//----------------------------------OTHER FUNCTIONS END----------------------------------------- //----------------------------------OTHER FUNCTIONS END-----------------------------------------

18
app/src/main/java/gr/thmmy/mthmmy/session/ValidateSessionTask.java

@ -0,0 +1,18 @@
package gr.thmmy.mthmmy.session;
import android.os.AsyncTask;
import gr.thmmy.mthmmy.base.BaseApplication;
public class ValidateSessionTask extends AsyncTask<String, Void, Void> {
@Override
protected Void doInBackground(String... params) {
BaseApplication.getInstance().getSessionManager().validateSession();
return null;
}
public boolean isRunning(){
return getStatus() == AsyncTask.Status.RUNNING;
}
}

47
app/src/main/java/gr/thmmy/mthmmy/utils/CircleTransform.java

@ -1,47 +0,0 @@
package gr.thmmy.mthmmy.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import com.squareup.picasso.Transformation;
/**
* Used as parameter for PICASSO library's {@link com.squareup.picasso.RequestCreator#transform(Transformation) transform} method.
* @see com.squareup.picasso.Picasso Picasso
*/
public class CircleTransform implements Transformation {
@Override
public Bitmap transform(Bitmap source) {
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
if (squaredBitmap != source)
source.recycle();
// For GIF images
Bitmap.Config config = source.getConfig() != null ? source.getConfig() : Bitmap.Config.ARGB_8888;
Bitmap bitmap = Bitmap.createBitmap(size, size, config);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
paint.setShader(shader);
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r, r, r, paint);
squaredBitmap.recycle();
return bitmap;
}
@Override
public String key() {
return "circle";
}
}

73
app/src/main/java/gr/thmmy/mthmmy/utils/DateTimeUtils.java

@ -0,0 +1,73 @@
package gr.thmmy.mthmmy.utils;
import androidx.annotation.VisibleForTesting;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
public class DateTimeUtils {
private static final long MONTH_IN_MILLIS = 30*DAY_IN_MILLIS;
private static final long DECADE_IN_MILLIS = 10*YEAR_IN_MILLIS;
@VisibleForTesting
public static String getRelativeTimeSpanString(long time) {
long now = System.currentTimeMillis();
boolean past = (now >= time);
long duration = Math.abs(now - time);
String format;
long count, mod;
if(duration < 45*SECOND_IN_MILLIS)
return "just now";
else if (duration < 45*MINUTE_IN_MILLIS) {
count = duration/MINUTE_IN_MILLIS;
mod = duration % MINUTE_IN_MILLIS;
if(mod >= 30*SECOND_IN_MILLIS)
count += 1;
format = "%dm";
} else if (duration < 22*HOUR_IN_MILLIS) {
count = duration/HOUR_IN_MILLIS;
format = "%dh";
mod = (duration%HOUR_IN_MILLIS)/MINUTE_IN_MILLIS;
if(count<3 && mod>9 && mod<51){
if(count==0)
format = mod +"m";
else
format = format + " " + mod +"m";
}
else if(mod >= 30)
count += 1;
} else if (duration < 26*DAY_IN_MILLIS) {
count = duration/DAY_IN_MILLIS;
format = "%dd";
mod = duration % DAY_IN_MILLIS;
if(mod >= 12*HOUR_IN_MILLIS)
count += 1;
} else if (duration < 320*DAY_IN_MILLIS) {
count = duration/MONTH_IN_MILLIS;
format = "%d month";
mod = duration % MONTH_IN_MILLIS;
if(mod >= 15*DAY_IN_MILLIS)
count += 1;
if(count>1)
format = format + 's';
} else if (duration < DECADE_IN_MILLIS) {
count = duration/YEAR_IN_MILLIS;
format = "%d year";
mod = duration % YEAR_IN_MILLIS;
if(mod >= 183*DAY_IN_MILLIS)
count += 1;
if(count>1)
format = format + 's';
}
else
return past ? "a long time ago": "in the distant future";
format = past ? format : "in " + format;
return String.format(format, (int) count);
}
}

9
app/src/main/java/gr/thmmy/mthmmy/utils/NetworkTask.java

@ -10,16 +10,15 @@ import java.io.IOException;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.crashreporting.CrashReporter;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
public abstract class NetworkTask<T> extends ExternalAsyncTask<String, Parcel<T>> { public abstract class NetworkTask<T> extends ExternalAsyncTask<String, Parcel<T>> {
private OnNetworkTaskFinishedListener<T> onNetworkTaskFinishedListener;
protected OnNetworkTaskFinishedListener<T> onNetworkTaskFinishedListener;
public NetworkTask(OnTaskStartedListener onTaskStartedListener, OnTaskCancelledListener onTaskCancelledListener, public NetworkTask(OnTaskStartedListener onTaskStartedListener, OnTaskCancelledListener onTaskCancelledListener,
OnNetworkTaskFinishedListener<T> onNetworkTaskFinishedListener) { OnNetworkTaskFinishedListener<T> onNetworkTaskFinishedListener) {
@ -47,14 +46,14 @@ public abstract class NetworkTask<T> extends ExternalAsyncTask<String, Parcel<T>
try { try {
responseBodyString = response.body().string(); responseBodyString = response.body().string();
} catch (NullPointerException npe) { } catch (NullPointerException npe) {
Timber.wtf(npe, "Invalid response. Detatails: https://square.github.io/okhttp/3.x/okhttp/okhttp3/Response.html#body--"); Timber.wtf(npe, "Invalid response. Details: https://square.github.io/okhttp/3.x/okhttp/okhttp3/Response.html#body--");
return new Parcel<>(NetworkResultCodes.NETWORK_ERROR, null); return new Parcel<>(NetworkResultCodes.NETWORK_ERROR, null);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "Error getting response body string"); Timber.e(e, "Error getting response body string");
return new Parcel<>(NetworkResultCodes.NETWORK_ERROR, null); return new Parcel<>(NetworkResultCodes.NETWORK_ERROR, null);
} }
try { try {
T data = performTask(ParseHelpers.parse(responseBodyString), response); T data = performTask(Jsoup.parse(responseBodyString), response);
int resultCode = getResultCode(response, data); int resultCode = getResultCode(response, data);
return new Parcel<>(resultCode, data); return new Parcel<>(resultCode, data);
} catch (ParseException pe) { } catch (ParseException pe) {

2
app/src/main/java/gr/thmmy/mthmmy/utils/CrashReporter.java → app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReporter.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils.crashreporting;
import com.crashlytics.android.Crashlytics; import com.crashlytics.android.Crashlytics;

2
app/src/main/java/gr/thmmy/mthmmy/utils/CrashReportingTree.java → app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReportingTree.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils.crashreporting;
import android.util.Log; import android.util.Log;

39
app/src/main/java/gr/thmmy/mthmmy/utils/io/AssetUtils.java

@ -0,0 +1,39 @@
package gr.thmmy.mthmmy.utils.io;
import android.content.Context;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import timber.log.Timber;
public class AssetUtils {
public static String readFileToText(Context context, String fileName) {
StringBuilder stringBuilder = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(context.getAssets().open(fileName)));
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
return stringBuilder.toString();
} catch (IOException e) {
Timber.e(e, "IO error reading file %s from assets.", fileName);
} catch (Exception e) {
Timber.e(e, "Error reading file %s from assets.", fileName);
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
Timber.e(e, "Error in AssetUtils (closing reader).");
}
}
return null;
}
}

54
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseHelpers.java

@ -1,9 +1,7 @@
package gr.thmmy.mthmmy.utils.parsing; package gr.thmmy.mthmmy.utils.parsing;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import java.util.ArrayList; import java.util.ArrayList;
@ -11,8 +9,6 @@ import java.util.HashMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import timber.log.Timber;
/** /**
* This class consists exclusively of static classes (enums) and methods (excluding methods of inner * 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 and state or fix embedded videos html code
@ -188,56 +184,6 @@ public class ParseHelpers {
else return ""; else return "";
} }
/**
* Method that replaces CloudFlare-obfuscated emails with deobfuscated ones
* Replace Jsoup.parse with this wherever needed
*
* @param html html to parse
* @return a document with deobfuscated emails
*/
public static Document parse(String html) {
Document document = Jsoup.parse(html);
deobfuscateElements(document.select("span.__cf_email__,a.__cf_email__"), true);
return document;
}
/**
* Use this method instead of parse() if you are targeting specific elements
*/
public static void deobfuscateElements(Elements elements, boolean found) {
if (!found)
elements = elements.select("span.__cf_email__,a.__cf_email__");
for (Element obfuscatedElement : elements) {
String deobfuscatedEmail = deobfuscateEmail(obfuscatedElement.attr("data-cfemail"));
if (obfuscatedElement.is("span")) {
Element parent = obfuscatedElement.parent();
if (parent.is("a") && parent.attr("href").contains("email-protection"))
parent.attr("href", "mailto:" + deobfuscatedEmail);
} else if (obfuscatedElement.attr("href").contains("email-protection"))
obfuscatedElement.attr("href", "mailto:" + deobfuscatedEmail);
obfuscatedElement.replaceWith(new TextNode(deobfuscatedEmail, ""));
}
}
/**
* @param obfuscatedEmail CloudFlare-obfuscated email
* @return deobfuscated email
*/
private static String deobfuscateEmail(String obfuscatedEmail) {
//Deobfuscate
final StringBuilder stringBuilder = new StringBuilder();
final int r = Integer.parseInt(obfuscatedEmail.substring(0, 2), 16);
for (int n = 2; n < obfuscatedEmail.length(); n += 2) {
final int i = Integer.parseInt(obfuscatedEmail.substring(n, n + 2), 16) ^ r;
stringBuilder.append(Character.toString((char) i));
}
Timber.d("Email deobfuscated.");
return stringBuilder.toString();
}
public static String emojiTagToHtml(String emojiTagedString) { public static String emojiTagToHtml(String emojiTagedString) {
HashMap<Pattern, String> tagToHtmlMap = new HashMap<>(); HashMap<Pattern, String> tagToHtmlMap = new HashMap<>();

5
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ParseTask.java

@ -3,6 +3,7 @@ package gr.thmmy.mthmmy.utils.parsing;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.widget.Toast; import android.widget.Toast;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import java.io.IOException; import java.io.IOException;
@ -20,7 +21,7 @@ import timber.log.Timber;
*/ */
public abstract class ParseTask extends AsyncTask<String, Void, ParseTask.ResultCode> { public abstract class ParseTask extends AsyncTask<String, Void, ParseTask.ResultCode> {
protected String url; protected String url;
protected enum ResultCode { public enum ResultCode {
SUCCESS, PARSING_ERROR, NETWORK_ERROR, OTHER_ERROR SUCCESS, PARSING_ERROR, NETWORK_ERROR, OTHER_ERROR
} }
@ -41,7 +42,7 @@ public abstract class ParseTask extends AsyncTask<String, Void, ParseTask.Result
Request request = prepareRequest(params); Request request = prepareRequest(params);
try { try {
Response response = BaseApplication.getInstance().getClient().newCall(request).execute(); Response response = BaseApplication.getInstance().getClient().newCall(request).execute();
Document document = ParseHelpers.parse(response.body().string()); Document document = Jsoup.parse(response.body().string());
parse(document); parse(document);
postParsing(); postParsing();
return ResultCode.SUCCESS; return ResultCode.SUCCESS;

112
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParser.java

@ -0,0 +1,112 @@
package gr.thmmy.mthmmy.utils.parsing;
import androidx.annotation.VisibleForTesting;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.joda.time.format.DateTimeParser;
import java.text.DateFormatSymbols;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.base.BaseApplication;
import timber.log.Timber;
public class ThmmyDateTimeParser {
private static final DateTimeParser[] parsers = {
DateTimeFormat.forPattern("hh:mm:ss a").getParser(),
DateTimeFormat.forPattern("HH:mm:ss").getParser(),
DateTimeFormat.forPattern("MMMM d, Y, hh:mm:ss a").getParser(),
DateTimeFormat.forPattern("MMMM d, Y, HH:mm:ss").getParser(),
DateTimeFormat.forPattern("d MMMM Y, HH:mm:ss").getParser(),
DateTimeFormat.forPattern("Y-M-d, HH:mm:ss").getParser(),
DateTimeFormat.forPattern("d-M-Y, HH:mm:ss").getParser()
};
private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.append(null, parsers)
.toFormatter();
//TODO: Replace with Locale.forLanguageTag() (with "el-GR","en-US") when KitKat support is dropped
private static final Locale greekLocale = new Locale("el", "GR");
private static final Locale englishLocale = new Locale("en", "US");
private static final Pattern pattern = Pattern.compile("\\s((1[3-9]|2[0-3]):)");
private ThmmyDateTimeParser(){}
public static String convertToTimestamp(String thmmyDateTime){
Timber.v("Will attempt to convert %s to timestamp.", thmmyDateTime);
String originalDateTime = thmmyDateTime;
DateTimeZone dtz = getDtz();
// Remove any unnecessary "Today at" strings
thmmyDateTime = purifyTodayDateTime(thmmyDateTime);
// Add today's date for the first two cases
if(thmmyDateTime.charAt(2)==':')
thmmyDateTime = (new DateTime()).toString("MMMM d, Y, ") + thmmyDateTime;
// Don't even ask
if(thmmyDateTime.contains("am"))
thmmyDateTime = thmmyDateTime.replaceAll("\\s00:"," 12:");
// For the stupid format 23:54:12 pm
Matcher matcher = pattern.matcher(thmmyDateTime);
if (matcher.find())
thmmyDateTime = thmmyDateTime.replaceAll("\\spm","");
DateTime dateTime;
try{
dateTime=formatter.withZone(dtz).withLocale(englishLocale).parseDateTime(thmmyDateTime);
}
catch (IllegalArgumentException e1){
Timber.v("Parsing DateTime %s using English Locale failed.", thmmyDateTime);
try{
DateFormatSymbols dfs = DateTimeUtils.getDateFormatSymbols(greekLocale);
thmmyDateTime = thmmyDateTime.replace("am",dfs.getAmPmStrings()[0]);
thmmyDateTime = thmmyDateTime.replace("pm",dfs.getAmPmStrings()[1]);
Timber.v("Attempting to parse DateTime %s using Greek Locale...", thmmyDateTime);
dateTime=formatter.withZone(dtz).withLocale(greekLocale).parseDateTime(thmmyDateTime);
}
catch (IllegalArgumentException e2){
Timber.d("Parsing DateTime %s using Greek Locale failed too.", thmmyDateTime);
Timber.e("Couldn't convert DateTime %s to timestamp!", originalDateTime);
return null;
}
}
String timestamp = Long.toString(dateTime.getMillis());
Timber.v("DateTime %s was converted to %s, or %s", originalDateTime, timestamp, dateTime.toString());
return timestamp;
}
public static String simplifyDateTime(String dateTime){
return removeSeconds(purifyTodayDateTime(dateTime));
}
// Converts e.g. Today at 12:16:48 -> 12:16:48, but October 03, 2019, 16:40:18 remains as is
@VisibleForTesting
static String purifyTodayDateTime(String dateTime){
return dateTime.replaceAll("(Today at |Σήμερα στις )(.+)", "$2");
}
// Converts e.g. 12:16:48 -> 12:16, October 03, 2019, 16:40:18 -> 12:16 October 03, 2019, 16:40
private static String removeSeconds(String dateTime){
return dateTime.replaceAll("(.*):\\d+($|\\s.*)", "$1$2");
}
@VisibleForTesting
private static DateTimeZone getDtz(){
if(!BaseApplication.getInstance().getSessionManager().isLoggedIn())
return DateTimeZone.forID("Europe/Athens");
else
return DateTimeZone.getDefault();
}
}

2
app/src/main/java/gr/thmmy/mthmmy/utils/CenterVerticalSpan.java → app/src/main/java/gr/thmmy/mthmmy/utils/ui/CenterVerticalSpan.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils.ui;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;

65
app/src/main/java/gr/thmmy/mthmmy/utils/ui/ImageDownloadDialogBuilder.java

@ -0,0 +1,65 @@
package gr.thmmy.mthmmy.utils.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import java.net.MalformedURLException;
import java.net.URL;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.ThmmyFile;
import timber.log.Timber;
import static android.content.Context.CLIPBOARD_SERVICE;
public class ImageDownloadDialogBuilder extends AlertDialog.Builder{
private static final String[] colors = {"Copy image location", "Save Image"};
private Context context;
private String imageURL;
public ImageDownloadDialogBuilder(@NonNull Context context, String imageURL) {
super(context);
this.context = context;
this.imageURL = imageURL;
setItems(colors, (dialog, which) -> {
if(which == 0)
copyUrlToClipboard();
else {
try {
getBaseActivity().downloadFile(new ThmmyFile(new URL(imageURL)));
} catch (MalformedURLException e) {
Timber.e(e, "Exception downloading image (MalformedURLException)");
} catch (NullPointerException e) {
Timber.e(e, "Exception downloading image (NullPointerException)");
}
}
});
}
private void copyUrlToClipboard(){
ClipboardManager clipboard = (ClipboardManager) BaseApplication.getInstance().getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("ReactiveWebViewCopiedText", imageURL);
clipboard.setPrimaryClip(clip);
Toast.makeText(BaseApplication.getInstance().getApplicationContext(),context.getString(R.string.link_copied_msg),Toast.LENGTH_SHORT).show();
}
private BaseActivity getBaseActivity() {
Context baseActivityContext = context;
while (baseActivityContext instanceof ContextWrapper) {
if (context instanceof BaseActivity)
return (BaseActivity) context;
baseActivityContext = ((ContextWrapper)context).getBaseContext();
}
return null;
}
}

63
app/src/main/java/gr/thmmy/mthmmy/utils/ui/PhotoViewUtils.java

@ -0,0 +1,63 @@
package gr.thmmy.mthmmy.utils.ui;
import android.app.Dialog;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.github.chrisbanes.photoview.PhotoView;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
public class PhotoViewUtils {
private final static int screenWidth = BaseApplication.getInstance().getWidthInPixels();
private final static int screenHeight = BaseApplication.getInstance().getHeightInPixels();
public static void displayPhotoViewImage(Context context, String imageURL) {
Dialog builder = new Dialog(context);
builder.requestWindowFeature(Window.FEATURE_NO_TITLE);
builder.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
builder.getWindow().setBackgroundDrawable(
new ColorDrawable(android.graphics.Color.TRANSPARENT));
PhotoView photoView = new PhotoView(context);
photoView.setLayoutParams(new ViewGroup.LayoutParams(screenWidth, screenHeight));
Glide.with(context)
.load(imageURL)
.fitCenter()
.error(R.drawable.ic_file_not_found)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
photoView.setZoomable(false);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
photoView.setOnLongClickListener(v -> {
ImageDownloadDialogBuilder imageDownloadDialogBuilder = new ImageDownloadDialogBuilder(context, imageURL);
imageDownloadDialogBuilder.show();
return false;
});
return false;
}
})
.into(photoView);
builder.addContentView(photoView, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
builder.show();
}
}

2
app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareFABBehavior.java → app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareFABBehavior.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils.ui;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;

2
app/src/main/java/gr/thmmy/mthmmy/utils/ScrollAwareLinearBehavior.java → app/src/main/java/gr/thmmy/mthmmy/utils/ui/ScrollAwareLinearBehavior.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils.ui;
import android.animation.Animator; import android.animation.Animator;
import android.content.Context; import android.content.Context;

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

@ -132,7 +132,7 @@ public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTa
int pageRequested = pageIndicatorIndex.getValue() - 1; int pageRequested = pageIndicatorIndex.getValue() - 1;
if (pageRequested != currentPageIndex - 1) { if (pageRequested != currentPageIndex - 1) {
Timber.i("Changing to page " + pageRequested + 1); Timber.i("Changing to page " + pageRequested + 1);
loadUrl(ParseHelpers.getBaseURL(topicUrl) + "." + String.valueOf(pageRequested * 15)); loadUrl(ParseHelpers.getBaseURL(topicUrl) + "." + pageRequested * 15);
pageIndicatorIndex.setValue(pageRequested + 1); pageIndicatorIndex.setValue(pageRequested + 1);
} else { } else {
stopLoading(); stopLoading();

2
app/src/main/java/gr/thmmy/mthmmy/utils/AppCompatSpinnerWithoutDefault.java → app/src/main/java/gr/thmmy/mthmmy/views/AppCompatSpinnerWithoutDefault.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.views;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;

2
app/src/main/java/gr/thmmy/mthmmy/utils/CustomLinearLayoutManager.java → app/src/main/java/gr/thmmy/mthmmy/views/CustomLinearLayoutManager.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.views;
import android.content.Context; import android.content.Context;

2
app/src/main/java/gr/thmmy/mthmmy/utils/CustomRecyclerView.java → app/src/main/java/gr/thmmy/mthmmy/views/CustomRecyclerView.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.views;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;

93
app/src/main/java/gr/thmmy/mthmmy/views/ReactiveWebView.java

@ -0,0 +1,93 @@
package gr.thmmy.mthmmy.views;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.webkit.WebView;
import android.widget.Toast;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.ui.ImageDownloadDialogBuilder;
import static android.content.Context.CLIPBOARD_SERVICE;
import static gr.thmmy.mthmmy.utils.ui.PhotoViewUtils.displayPhotoViewImage;
public class ReactiveWebView extends WebView {
private final static long MAX_TOUCH_DURATION = 100;
private final Context context;
private long downTime;
public ReactiveWebView(Context context) {
super(context);
this.context = context;
init();
}
public ReactiveWebView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
public ReactiveWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init(){
setOnLongClickListener();
this.setVerticalScrollBarEnabled(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downTime = event.getEventTime();
break;
case MotionEvent.ACTION_UP:
if(event.getEventTime() - downTime <= MAX_TOUCH_DURATION)
performClick();
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean performClick() {
WebView.HitTestResult result = this.getHitTestResult();
if(result.getType() == WebView.HitTestResult.IMAGE_TYPE){
String imageURL = result.getExtra();
displayPhotoViewImage(context, imageURL);
}
return super.performClick();
}
private void setOnLongClickListener(){
this.setOnLongClickListener(v -> {
HitTestResult result = ReactiveWebView.this.getHitTestResult();
if(result.getType() == HitTestResult.SRC_ANCHOR_TYPE)
copyUrlToClipboard(result.getExtra());
else if(result.getType() == WebView.HitTestResult.IMAGE_TYPE) {
String imageURL = result.getExtra();
ImageDownloadDialogBuilder builder = new ImageDownloadDialogBuilder(context,imageURL);
builder.show();
}
return false;
});
}
private void copyUrlToClipboard(String urlToCopy){
ClipboardManager clipboard = (ClipboardManager) BaseApplication.getInstance().getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("ReactiveWebViewCopiedText", urlToCopy);
clipboard.setPrimaryClip(clip);
Toast.makeText(BaseApplication.getInstance().getApplicationContext(),context.getString(R.string.link_copied_msg),Toast.LENGTH_SHORT).show();
}
}

232
app/src/main/java/gr/thmmy/mthmmy/views/RelativeTimeTextView.java

@ -0,0 +1,232 @@
package gr.thmmy.mthmmy.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import gr.thmmy.mthmmy.R;
import static gr.thmmy.mthmmy.utils.DateTimeUtils.getRelativeTimeSpanString;
/**
* A modified version of https://github.com/curioustechizen/android-ago
*/
@SuppressLint("AppCompatCustomView")
public class RelativeTimeTextView extends TextView {
private static final long INITIAL_UPDATE_INTERVAL = DateUtils.MINUTE_IN_MILLIS;
private long mReferenceTime;
private Handler mHandler = new Handler();
private UpdateTimeRunnable mUpdateTimeTask;
private boolean isUpdateTaskRunning = false;
public RelativeTimeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RelativeTimeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.RelativeTimeTextView, 0, 0);
String referenceTimeText;
try {
referenceTimeText = a.getString(R.styleable.RelativeTimeTextView_reference_time);
} finally {
a.recycle();
}
try {
mReferenceTime = Long.valueOf(referenceTimeText);
} catch (NumberFormatException nfe) {
/*
* TODO: Better exception handling
*/
mReferenceTime = -1L;
}
setReferenceTime(mReferenceTime);
}
/**
* Sets the reference time for this view. At any moment, the view will render a relative time period relative to the time set here.
* <p/>
* This value can also be set with the XML attribute {@code reference_time}
* @param referenceTime The timestamp (in milliseconds since epoch) that will be the reference point for this view.
*/
public void setReferenceTime(long referenceTime) {
this.mReferenceTime = referenceTime;
/*
* Note that this method could be called when a row in a ListView is recycled.
* Hence, we need to first stop any currently running schedules (for example from the recycled view.
*/
stopTaskForPeriodicallyUpdatingRelativeTime();
/*
* Instantiate a new runnable with the new reference time
*/
initUpdateTimeTask();
/*
* Start a new schedule.
*/
startTaskForPeriodicallyUpdatingRelativeTime();
/*
* Finally, update the text display.
*/
updateTextDisplay();
}
private void updateTextDisplay() {
/*
* TODO: Validation, Better handling of negative cases
*/
if (this.mReferenceTime == -1L)
return;
setText(getRelativeTimeSpanString(mReferenceTime));
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startTaskForPeriodicallyUpdatingRelativeTime();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopTaskForPeriodicallyUpdatingRelativeTime();
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == GONE || visibility == INVISIBLE) {
stopTaskForPeriodicallyUpdatingRelativeTime();
} else {
startTaskForPeriodicallyUpdatingRelativeTime();
}
}
private void startTaskForPeriodicallyUpdatingRelativeTime() {
if(mUpdateTimeTask.isDetached()) initUpdateTimeTask();
mHandler.post(mUpdateTimeTask);
isUpdateTaskRunning = true;
}
private void initUpdateTimeTask() {
mUpdateTimeTask = new UpdateTimeRunnable(this, mReferenceTime);
}
private void stopTaskForPeriodicallyUpdatingRelativeTime() {
if(isUpdateTaskRunning) {
mUpdateTimeTask.detach();
mHandler.removeCallbacks(mUpdateTimeTask);
isUpdateTaskRunning = false;
}
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.referenceTime = mReferenceTime;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
mReferenceTime = ss.referenceTime;
super.onRestoreInstanceState(ss.getSuperState());
}
public static class SavedState extends BaseSavedState {
private long referenceTime;
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(referenceTime);
}
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
private SavedState(Parcel in) {
super(in);
referenceTime = in.readLong();
}
}
private static class UpdateTimeRunnable implements Runnable {
private long mRefTime;
private final WeakReference<RelativeTimeTextView> weakRefRttv;
UpdateTimeRunnable(RelativeTimeTextView rttv, long refTime) {
this.mRefTime = refTime;
weakRefRttv = new WeakReference<>(rttv);
}
boolean isDetached() {
return weakRefRttv.get() == null;
}
void detach() {
weakRefRttv.clear();
}
@Override
public void run() {
RelativeTimeTextView rttv = weakRefRttv.get();
if (rttv == null) return;
long difference = Math.abs(System.currentTimeMillis() - mRefTime);
long interval = INITIAL_UPDATE_INTERVAL;
if (difference > DateUtils.WEEK_IN_MILLIS) {
interval = DateUtils.WEEK_IN_MILLIS;
} else if (difference > DateUtils.DAY_IN_MILLIS) {
interval = DateUtils.DAY_IN_MILLIS;
} else if (difference > DateUtils.HOUR_IN_MILLIS) {
interval = DateUtils.HOUR_IN_MILLIS;
}
rttv.updateTextDisplay();
rttv.mHandler.postDelayed(this, interval);
}
}
}

2
app/src/main/java/gr/thmmy/mthmmy/utils/ToggledBackgroundButton.java → app/src/main/java/gr/thmmy/mthmmy/views/ToggledBackgroundButton.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.views;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EditorView.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EditorView.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview; package gr.thmmy.mthmmy.views.editorview;
import android.animation.Animator; import android.animation.Animator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EmojiInputField.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiInputField.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview; package gr.thmmy.mthmmy.views.editorview;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EmojiKeyboard.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboard.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview; package gr.thmmy.mthmmy.views.editorview;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/EmojiKeyboardAdapter.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/EmojiKeyboardAdapter.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview; package gr.thmmy.mthmmy.views.editorview;
import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.AnimationDrawable;
import android.view.LayoutInflater; import android.view.LayoutInflater;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/FormatButtonsAdapter.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/FormatButtonsAdapter.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview; package gr.thmmy.mthmmy.views.editorview;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;

2
app/src/main/java/gr/thmmy/mthmmy/editorview/IEmojiKeyboard.java → app/src/main/java/gr/thmmy/mthmmy/views/editorview/IEmojiKeyboard.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.editorview; package gr.thmmy.mthmmy.views.editorview;
public interface IEmojiKeyboard { public interface IEmojiKeyboard {
/** /**

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

@ -0,0 +1,5 @@
<vector android:height="96dp" android:tint="#D926A69A"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z"/>
</vector>

26
app/src/main/res/drawable/ic_launcher_foreground.xml

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1039.6812"
android:viewportHeight="1039.6812">
<group android:translateX="161.25058"
android:translateY="161.15057">
<path
android:pathData="M0,358.69a358.58,358.69 0,1 0,717.16 0a358.58,358.69 0,1 0,-717.16 0z"
android:fillColor="#333"/>
<group>
<clip-path
android:pathData="M0,358.69a358.58,358.69 0,1 0,717.16 0a358.58,358.69 0,1 0,-717.16 0z"/>
<path
android:pathData="M358.66,367.61c1,0 26,-75 37,-108.48 1.54,-4.68 7,-5.52 8.66,-0.27L493.66,473.03a4.29,4.29 0,0 0,7.93 0l27.28,-66.77a4.1,4.1 0,0 1,4 -2.43c61.43,0.08 122.87,-0.08 184.31,0"
android:strokeWidth="28"
android:fillColor="#00000000"
android:strokeColor="#6bdad5"/>
<path
android:pathData="M358.66,367.61c-1,0 -26,-75 -37,-108.48 -1.54,-4.68 -7,-5.52 -8.66,-0.27L223.66,473.03a4.29,4.29 0,0 1,-7.93 0l-27.28,-66.77a4.1,4.1 0,0 0,-4 -2.43c-61.43,0.08 -122.87,-0.08 -184.31,0"
android:strokeWidth="28"
android:fillColor="#00000000"
android:strokeColor="#6bdad5"/>
</group>
</group>
</vector>

5
app/src/main/res/drawable/ic_mark_as_read.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="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM2,16h8v-2L2,14v2zM21.5,11.5L23,13l-6.99,7 -4.51,-4.5L13,14l3.01,3 5.49,-5.5z"/>
</vector>

9
app/src/main/res/layout-v21/activity_profile.xml

@ -37,7 +37,7 @@
android:id="@+id/user_thumbnail" android:id="@+id/user_thumbnail"
android:layout_width="@dimen/profile_activity_avatar_size" android:layout_width="@dimen/profile_activity_avatar_size"
android:layout_height="@dimen/profile_activity_avatar_size" android:layout_height="@dimen/profile_activity_avatar_size"
android:layout_marginBottom="5dp" android:layout_marginBottom="6dp"
android:layout_gravity="center" android:layout_gravity="center"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail" android:contentDescription="@string/post_thumbnail"
@ -50,6 +50,11 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="6dp"
android:paddingBottom="4dp"
android:textAlignment="center"
android:textColor="@color/primary_text" android:textColor="@color/primary_text"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>
@ -113,7 +118,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margins" android:layout_margin="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior" app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_pm_fab"/> app:srcCompat="@drawable/ic_pm_fab"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

2
app/src/main/res/layout-v21/activity_topic_post_row.xml

@ -225,7 +225,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginBottom="16dp"> android:layout_marginBottom="16dp">
<WebView <gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/post" android:id="@+id/post"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

30
app/src/main/res/layout/activity_about.xml

@ -110,30 +110,54 @@
<TextView <TextView
android:id="@+id/apache_libs" android:id="@+id/apache_libs"
android:tag="APACHE"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/libraries_text" android:layout_below="@+id/libraries_text"
android:onClick="displayApacheLibraries" android:onClick="displayLibraries"
android:text="@string/apache_v2_0_libraries" android:text="@string/apache_v2_0_libraries"
android:textColor="@color/accent" /> android:textColor="@color/accent" />
<TextView <TextView
android:id="@+id/mit_libs" android:id="@+id/mit_libs"
android:tag="MIT"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/apache_libs" android:layout_below="@+id/apache_libs"
android:onClick="displayMITLibraries" android:onClick="displayLibraries"
android:text="@string/the_mit_libraries" android:text="@string/the_mit_libraries"
android:textColor="@color/accent" /> android:textColor="@color/accent" />
<TextView <TextView
android:id="@+id/privacy_policy_header" android:id="@+id/epl_libs"
android:tag="EPL"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/mit_libs" android:layout_below="@+id/mit_libs"
android:onClick="displayLibraries"
android:text="@string/epl_libraries"
android:textColor="@color/accent" />
<TextView
android:id="@+id/other_libs"
android:tag="OTHER"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/epl_libs"
android:onClick="displayLibraries"
android:text="@string/other_libraries"
android:textColor="@color/accent" />
<TextView
android:id="@+id/privacy_policy_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/other_libs"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:text="@string/privacy_policy" android:text="@string/privacy_policy"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"

2
app/src/main/res/layout/activity_board.xml

@ -62,6 +62,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margins" android:layout_margin="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior" app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_add_fab" /> app:srcCompat="@drawable/ic_add_fab" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

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

@ -46,7 +46,7 @@
android:inputType="text"/> android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<gr.thmmy.mthmmy.editorview.EditorView <gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/main_content_editorview" android:id="@+id/main_content_editorview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -55,7 +55,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:hint="topic message"/> app:hint="topic message"/>
<gr.thmmy.mthmmy.editorview.EmojiKeyboard <gr.thmmy.mthmmy.views.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard" android:id="@+id/emoji_keyboard"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

2
app/src/main/res/layout/activity_downloads.xml

@ -55,6 +55,6 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/fab_margins" android:layout_marginBottom="@dimen/fab_margins"
android:layout_marginEnd="@dimen/fab_margins" android:layout_marginEnd="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior" app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_file_upload_white_24dp"/> app:srcCompat="@drawable/ic_file_upload_white_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

9
app/src/main/res/layout/activity_profile.xml

@ -37,7 +37,7 @@
android:id="@+id/user_thumbnail" android:id="@+id/user_thumbnail"
android:layout_width="@dimen/profile_activity_avatar_size" android:layout_width="@dimen/profile_activity_avatar_size"
android:layout_height="@dimen/profile_activity_avatar_size" android:layout_height="@dimen/profile_activity_avatar_size"
android:layout_marginBottom="5dp" android:layout_marginBottom="6dp"
android:layout_gravity="center" android:layout_gravity="center"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail" android:contentDescription="@string/post_thumbnail"
@ -49,6 +49,11 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="6dp"
android:paddingBottom="4dp"
android:textAlignment="center"
android:textColor="@color/primary_text" android:textColor="@color/primary_text"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>
@ -112,7 +117,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margins" android:layout_margin="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior" app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_pm_fab"/> app:srcCompat="@drawable/ic_pm_fab"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

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

@ -51,7 +51,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="gr.thmmy.mthmmy.activities.topic.TopicActivity" /> tools:context="gr.thmmy.mthmmy.activities.topic.TopicActivity" />
<gr.thmmy.mthmmy.editorview.EmojiKeyboard <gr.thmmy.mthmmy.views.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard" android:id="@+id/emoji_keyboard"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="240dp" android:layout_height="240dp"
@ -78,7 +78,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:background="@color/primary" android:background="@color/primary"
app:elevation="8dp" app:elevation="8dp"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareLinearBehavior"> app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareLinearBehavior">
<ImageButton <ImageButton
android:id="@+id/page_first_button" android:id="@+id/page_first_button"
@ -148,7 +148,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margins" android:layout_marginEnd="@dimen/fab_margins"
android:layout_marginBottom="50dp" android:layout_marginBottom="50dp"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior" app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_reply" /> app:srcCompat="@drawable/ic_reply" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

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

@ -76,7 +76,7 @@
android:textSize="10sp" android:textSize="10sp"
tools:ignore="SmallSp" /> tools:ignore="SmallSp" />
</RelativeLayout> </RelativeLayout>
<gr.thmmy.mthmmy.editorview.EditorView <gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/edit_editorview" android:id="@+id/edit_editorview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

2
app/src/main/res/layout/activity_topic_post_row.xml

@ -225,7 +225,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginBottom="16dp"> android:layout_marginBottom="16dp">
<WebView <gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/post" android:id="@+id/post"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

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

@ -76,7 +76,7 @@
android:textSize="10sp" android:textSize="10sp"
tools:ignore="SmallSp" /> tools:ignore="SmallSp" />
</RelativeLayout> </RelativeLayout>
<gr.thmmy.mthmmy.editorview.EditorView <gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/reply_editorview" android:id="@+id/reply_editorview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

6
app/src/main/res/layout/activity_upload.xml

@ -49,7 +49,7 @@
android:background="@color/primary_light" android:background="@color/primary_light"
android:orientation="vertical"> android:orientation="vertical">
<gr.thmmy.mthmmy.utils.AppCompatSpinnerWithoutDefault <gr.thmmy.mthmmy.views.AppCompatSpinnerWithoutDefault
android:id="@+id/upload_spinner_category_root" android:id="@+id/upload_spinner_category_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -118,7 +118,7 @@
android:inputType="textMultiLine" /> android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<gr.thmmy.mthmmy.utils.ToggledBackgroundButton <gr.thmmy.mthmmy.views.ToggledBackgroundButton
android:id="@+id/upload_title_description_builder" android:id="@+id/upload_title_description_builder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -195,7 +195,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margins" android:layout_marginEnd="@dimen/fab_margins"
android:layout_marginBottom="@dimen/fab_margins" android:layout_marginBottom="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ScrollAwareFABBehavior" app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_file_upload_white_24dp" /> app:srcCompat="@drawable/ic_file_upload_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

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

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_height="match_parent"> android:layout_width="match_parent" android:layout_height="match_parent">
@ -22,4 +22,15 @@
android:divider="?android:listDivider" android:divider="?android:listDivider"
android:dividerPadding="16dp"/> android:dividerPadding="16dp"/>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</RelativeLayout> <TextView
android:id="@+id/nothing_bookmarked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="invisible"
android:textIsSelectable="false"
android:text="@string/nothing_bookmarked_here"
android:textColor="@color/accent"
android:textSize="@dimen/medium_text"
/>
</FrameLayout>

2
app/src/main/res/layout/fragment_forum.xml

@ -11,7 +11,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<gr.thmmy.mthmmy.utils.CustomRecyclerView <gr.thmmy.mthmmy.views.CustomRecyclerView
android:id="@+id/list" android:id="@+id/list"
android:name="gr.thmmy.mthmmy.sections.forum.ForumFragment" android:name="gr.thmmy.mthmmy.sections.forum.ForumFragment"
android:layout_width="match_parent" android:layout_width="match_parent"

2
app/src/main/res/layout/fragment_latest_posts_row.xml

@ -50,7 +50,7 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@+id/spacer_divider"> android:layout_below="@+id/spacer_divider">
<WebView <gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/post" android:id="@+id/post"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

2
app/src/main/res/layout/fragment_recent.xml

@ -11,7 +11,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<gr.thmmy.mthmmy.utils.CustomRecyclerView <gr.thmmy.mthmmy.views.CustomRecyclerView
android:id="@+id/list" android:id="@+id/list"
android:name="gr.thmmy.mthmmy.sections.recent.RecentFragment" android:name="gr.thmmy.mthmmy.sections.recent.RecentFragment"
android:layout_width="match_parent" android:layout_width="match_parent"

2
app/src/main/res/layout/fragment_recent_row.xml

@ -32,7 +32,7 @@
android:layout_below="@+id/title" android:layout_below="@+id/title"
android:layout_toEndOf="@+id/dateTime"/> android:layout_toEndOf="@+id/dateTime"/>
<TextView <gr.thmmy.mthmmy.views.RelativeTimeTextView
android:id="@+id/dateTime" android:id="@+id/dateTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

7
app/src/main/res/layout/fragment_shoutbox.xml

@ -16,23 +16,24 @@
app:mpb_indeterminateTint="@color/accent" app:mpb_indeterminateTint="@color/accent"
app:mpb_progressStyle="horizontal" /> app:mpb_progressStyle="horizontal" />
<gr.thmmy.mthmmy.utils.CustomRecyclerView <gr.thmmy.mthmmy.views.CustomRecyclerView
android:id="@+id/shoutbox_recyclerview" android:id="@+id/shoutbox_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
tools:listitem="@layout/fragment_shoutbox_shout_row" /> tools:listitem="@layout/fragment_shoutbox_shout_row" />
<gr.thmmy.mthmmy.editorview.EditorView <gr.thmmy.mthmmy.views.editorview.EditorView
android:id="@+id/edior_view" android:id="@+id/edior_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:visibility="gone"/> android:visibility="gone"/>
<gr.thmmy.mthmmy.editorview.EmojiKeyboard <gr.thmmy.mthmmy.views.editorview.EmojiKeyboard
android:id="@+id/emoji_keyboard" android:id="@+id/emoji_keyboard"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="180dp" android:layout_height="180dp"

2
app/src/main/res/layout/fragment_shoutbox_shout_row.xml

@ -50,7 +50,7 @@
android:layout_marginBottom="9dp" android:layout_marginBottom="9dp"
android:background="@color/divider" /> android:background="@color/divider" />
<WebView <gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/shout_content" android:id="@+id/shout_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save