Browse Source

Merge branch 'develop' into pms

pms
Thodoris Tyrovouzis 5 years ago
parent
commit
7b42127566
  1. 17
      app/build.gradle
  2. 10
      app/src/main/assets/apache_libraries.html
  3. 261
      app/src/main/assets/epl_libraries.html
  4. 6
      app/src/main/assets/mit_libraries.html
  5. 37
      app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java
  6. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java
  7. 40
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksActivity.java
  8. 86
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java
  9. 158
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksTopicFragment.java
  10. 33
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentAdapter.java
  11. 24
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java
  12. 31
      app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadAdapter.java
  13. 20
      app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadFragment.java
  14. 11
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java
  15. 1
      app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsActivity.java
  16. 21
      app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsFragment.java
  17. 39
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  18. 24
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java
  19. 6
      app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadFieldsBuilderActivity.java
  20. 28
      app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java
  21. 14
      app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java
  22. 73
      app/src/main/java/gr/thmmy/mthmmy/utils/DateTimeUtils.java
  23. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/NetworkTask.java
  24. 232
      app/src/main/java/gr/thmmy/mthmmy/utils/RelativeTimeTextView.java
  25. 106
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParser.java
  26. 19
      app/src/main/res/layout/activity_about.xml
  27. 2
      app/src/main/res/layout/fragment_recent_row.xml
  28. 2
      app/src/main/res/layout/fragment_unread_row.xml
  29. 3
      app/src/main/res/values/attrs.xml
  30. 14
      app/src/main/res/values/strings.xml
  31. 10
      app/src/main/res/xml-v26/app_preferences_guest.xml
  32. 18
      app/src/main/res/xml-v26/app_preferences_user.xml
  33. 16
      app/src/main/res/xml/app_preferences_guest.xml
  34. 24
      app/src/main/res/xml/app_preferences_user.xml
  35. 106
      app/src/test/java/gr/thmmy/mthmmy/utils/DateTimeUtilsTest.java
  36. 85
      app/src/test/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParserTest.java
  37. 8
      build.gradle
  38. 1
      emojis/.gitignore
  39. 29
      emojis/build.gradle
  40. 0
      emojis/consumer-rules.pro
  41. 21
      emojis/proguard-rules.pro
  42. 2
      emojis/src/main/AndroidManifest.xml
  43. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper.xml
  44. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f0.png
  45. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f1.png
  46. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f10.png
  47. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f11.png
  48. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f12.png
  49. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f13.png
  50. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f14.png
  51. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f15.png
  52. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f16.png
  53. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f17.png
  54. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f18.png
  55. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f19.png
  56. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f2.png
  57. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f20.png
  58. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f21.png
  59. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f22.png
  60. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f23.png
  61. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f24.png
  62. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f25.png
  63. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f26.png
  64. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f27.png
  65. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f28.png
  66. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f29.png
  67. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f3.png
  68. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f30.png
  69. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f31.png
  70. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f32.png
  71. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f33.png
  72. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f34.png
  73. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f35.png
  74. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f36.png
  75. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f37.png
  76. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f38.png
  77. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f39.png
  78. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f4.png
  79. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f40.png
  80. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f41.png
  81. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f42.png
  82. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f43.png
  83. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f44.png
  84. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f45.png
  85. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f46.png
  86. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f47.png
  87. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f48.png
  88. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f49.png
  89. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f5.png
  90. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f50.png
  91. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f51.png
  92. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f52.png
  93. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f53.png
  94. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f54.png
  95. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f55.png
  96. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f56.png
  97. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f57.png
  98. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f58.png
  99. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f59.png
  100. 0
      emojis/src/main/res/drawable/emoji_a_eatpaper_f6.png

17
app/build.gradle

@ -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
@ -79,15 +80,16 @@ 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.0'
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.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha04' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-beta01'
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.1.0-rc01'
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.firebase:firebase-core:17.0.0' implementation 'com.google.firebase:firebase-core:17.0.0'
implementation 'com.google.firebase:firebase-messaging:19.0.1' implementation 'com.google.firebase:firebase-messaging:19.0.1'
@ -97,6 +99,7 @@ dependencies {
implementation 'com.squareup.picasso:picasso:2.5.2' implementation 'com.squareup.picasso:picasso:2.5.2'
implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' 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'
@ -111,13 +114,11 @@ dependencies {
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.5' //Plugin: https://plugins.jetbrains.com/plugin/11249-okhttp-profiler
// Required for local unit tests (JUnit 4 framework)
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.powermock:powermock-core:2.0.2'
// Required for instrumented tests testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
androidTestImplementation 'com.android.support:support-annotations:28.0.0' testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
androidTestImplementation 'com.android.support.test:runner:1.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'

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

@ -1,5 +1,4 @@
<html> <html>
<head> <head>
<style> <style>
body { body {
@ -71,6 +70,15 @@
<li> <li>
<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>
<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> <li>
<h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.5</h5> <h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.5</h5>
</li> </li>

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

@ -0,0 +1,261 @@
<html>
<head>
<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>
<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>

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

@ -1,5 +1,4 @@
<html> <html>
<head> <head>
<style> <style>
body { body {
@ -39,7 +38,7 @@
<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.12 (Copyright ©2013 -2018 Karol Wrótniak, Droids on Roids)</h5>
@ -47,6 +46,9 @@
<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>

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

@ -39,6 +39,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 +135,33 @@ 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="", fileUrl="";
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) fileUrl="file:///android_asset/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(); fileUrl="file:///android_asset/mit_libraries.html";
if(alertDialog.getWindow()!=null) break;
alertDialog.getWindow().setLayout(width, height); case "EPL":
title=getString(R.string.epl_libraries);
fileUrl="file:///android_asset/epl_libraries.html";
break;
default:
break;
} }
public void displayMITLibraries(View v) {
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.loadUrl(fileUrl);
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();

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

@ -96,7 +96,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);

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

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

@ -21,38 +21,53 @@ 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 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 +76,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,47 +98,45 @@ 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);
if(this.boardBookmarks != null && !this.boardBookmarks.isEmpty()) { if(this.bookmarks != null && !this.bookmarks.isEmpty()) {
for (final Bookmark bookmarkedBoard : boardBookmarks) { for (final Bookmark bookmark : bookmarks) {
if (bookmarkedBoard != null && bookmarkedBoard.getTitle() != null) { 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()); bookmarksLinearView.addView(bookmarksListEmptyMessage());
} }
}); });
@ -142,7 +155,11 @@ public class BookmarksBoardFragment extends Fragment {
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 12, 0, 0); params.setMargins(0, 12, 0, 0);
emptyBookmarksCategory.setLayoutParams(params); emptyBookmarksCategory.setLayoutParams(params);
if(type==Type.TOPIC)
emptyBookmarksCategory.setText(getString(R.string.empty_topic_bookmarks));
else if(type==Type.BOARD)
emptyBookmarksCategory.setText(getString(R.string.empty_board_bookmarks)); emptyBookmarksCategory.setText(getString(R.string.empty_board_bookmarks));
emptyBookmarksCategory.setTypeface(emptyBookmarksCategory.getTypeface(), Typeface.BOLD); emptyBookmarksCategory.setTypeface(emptyBookmarksCategory.getTypeface(), Typeface.BOLD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
emptyBookmarksCategory.setTextColor(this.getContext().getColor(R.color.primary_text)); emptyBookmarksCategory.setTextColor(this.getContext().getColor(R.color.primary_text));
@ -153,4 +170,5 @@ public class BookmarksBoardFragment extends Fragment {
emptyBookmarksCategory.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); emptyBookmarksCategory.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
return emptyBookmarksCategory; return emptyBookmarksCategory;
} }
} }

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

33
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.utils.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;
} }
@ -43,22 +43,27 @@ 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()); holder.mTitleView.setText(recentList.get(position).getSubject());
holder.mDateTimeView.setText(recentList.get(position).getDateTimeModified());
holder.mUserView.setText(recentList.get(position).getLastUser());
holder.topic = recentList.get(position); String dateTimeString = recentList.get(position).getDateTimeModified();
if(BaseApplication.getInstance().isDisplayRelativeTimeEnabled())
try{
holder.mDateTimeView.setReferenceTime(Long.valueOf(dateTimeString));
}
catch(NumberFormatException e){
Timber.e(e, "Invalid number format: %s", dateTimeString);
holder.mDateTimeView.setText(dateTimeString);
}
else
holder.mDateTimeView.setText(dateTimeString);
holder.mView.setOnClickListener(new View.OnClickListener() { holder.mUserView.setText(recentList.get(position).getLastUser());
@Override holder.topic = recentList.get(position);
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 +77,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) {

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

@ -1,6 +1,7 @@
package gr.thmmy.mthmmy.activities.main.recent; package gr.thmmy.mthmmy.activities.main.recent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -34,6 +35,9 @@ import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertDateTime;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
/** /**
* A {@link BaseFragment} subclass. * A {@link BaseFragment} subclass.
@ -100,7 +104,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,7 +132,7 @@ public class RecentFragment extends BaseFragment {
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (recentTask.isRunning()) if (recentTask!=null && recentTask.isRunning())
recentTask.cancel(true); recentTask.cancel(true);
} }
@ -188,16 +192,16 @@ public class RecentFragment extends BaseFragment {
matcher = pattern.matcher(dateTime); matcher = pattern.matcher(dateTime);
if (matcher.find()){ if (matcher.find()){
dateTime = matcher.group(1); dateTime = matcher.group(1);
if (dateTime.contains(" am") || dateTime.contains(" pm") || if (BaseApplication.getInstance().isDisplayRelativeTimeEnabled()) {
dateTime.contains(" πμ") || dateTime.contains(" μμ")) { dateTime=convertDateTime(dateTime, false);
dateTime = dateTime.replaceAll(":[0-5][0-9] ", " "); String timestamp = convertToTimestamp(dateTime);
} else { if(timestamp!=null)
dateTime = dateTime.substring(0, dateTime.lastIndexOf(":")); dateTime=timestamp;
} }
if (!dateTime.contains(",")) { else
dateTime = dateTime.replaceAll(".+? ([0-9])", "$1"); dateTime=convertDateTime(dateTime, true);
} }
} else else
throw new ParseException("Parsing failed (dateTime)"); throw new ParseException("Parsing failed (dateTime)");
fetchedRecent.add(new TopicSummary(link, title, lastUser, dateTime)); fetchedRecent.add(new TopicSummary(link, title, lastUser, dateTime));

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

@ -11,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.utils.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;
@ -65,35 +68,41 @@ class UnreadAdapter extends RecyclerView.Adapter<RecyclerView.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(unreadList.get(holder.getAdapterPosition()).getSubject());
viewHolder.mDateTimeView.setText(unreadList.get(holder.getAdapterPosition()).getDateTimeModified());
viewHolder.mUserView.setText(unreadList.get(position).getLastUser());
String dateTimeString=unreadList.get(holder.getAdapterPosition()).getDateTimeModified();
if(BaseApplication.getInstance().isDisplayRelativeTimeEnabled()){
try{
viewHolder.mDateTimeView.setReferenceTime(Long.valueOf(dateTimeString));
}
catch(NumberFormatException e){
Timber.e(e, "Invalid number format.");
viewHolder.mDateTimeView.setText(dateTimeString);
}
}
else
viewHolder.mDateTimeView.setText(dateTimeString);
viewHolder.mUserView.setText(unreadList.get(position).getLastUser());
viewHolder.topic = unreadList.get(holder.getAdapterPosition()); viewHolder.topic = unreadList.get(holder.getAdapterPosition());
viewHolder.mView.setOnClickListener(new View.OnClickListener() { viewHolder.mView.setOnClickListener(v -> {
@Override
public void onClick(View 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.onUnreadFragmentInteraction(viewHolder.topic); //? mListener.onUnreadFragmentInteraction(viewHolder.topic); //?
} }
}
}); });
} else if (holder instanceof UnreadAdapter.MarkReadViewHolder) { } else if (holder instanceof UnreadAdapter.MarkReadViewHolder) {
final UnreadAdapter.MarkReadViewHolder markReadViewHolder = (UnreadAdapter.MarkReadViewHolder) holder; final UnreadAdapter.MarkReadViewHolder markReadViewHolder = (UnreadAdapter.MarkReadViewHolder) holder;
markReadViewHolder.text.setText(unreadList.get(holder.getAdapterPosition()).getSubject()); markReadViewHolder.text.setText(unreadList.get(holder.getAdapterPosition()).getSubject());
markReadViewHolder.topic = unreadList.get(holder.getAdapterPosition()); markReadViewHolder.topic = unreadList.get(holder.getAdapterPosition());
markReadViewHolder.mView.setOnClickListener(new View.OnClickListener() { markReadViewHolder.mView.setOnClickListener(v -> {
@Override
public void onClick(View 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()); markReadListener.onMarkReadInteraction(unreadList.get(holder.getAdapterPosition()).getTopicUrl());
} }
}
}); });
} }
} }
@ -107,7 +116,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) {

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

@ -2,6 +2,7 @@
package gr.thmmy.mthmmy.activities.main.unread; package gr.thmmy.mthmmy.activities.main.unread;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -24,6 +25,7 @@ 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;
@ -36,6 +38,9 @@ import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertDateTime;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
/** /**
* A {@link BaseFragment} subclass. * A {@link BaseFragment} subclass.
* Activities that contain this fragment must implement the * Activities that contain this fragment must implement the
@ -211,17 +216,18 @@ 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(" μμ")) if (BaseApplication.getInstance().isDisplayRelativeTimeEnabled()) {
dateTime = dateTime.replaceAll(":[0-5][0-9] ", " "); dateTime=convertDateTime(dateTime, false);
String timestamp = convertToTimestamp(dateTime);
if(timestamp!=null)
dateTime=timestamp;
}
else else
dateTime = dateTime.substring(0, dateTime.lastIndexOf(":")); dateTime=convertDateTime(dateTime, true);
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));
} }

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

@ -25,6 +25,7 @@ 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;
@ -176,13 +177,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;

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

39
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;
@ -123,6 +125,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 +164,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);
@ -253,8 +257,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 +316,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();
} }
@ -788,4 +796,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.url_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;
});
}
} }

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

@ -6,6 +6,9 @@ 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 +44,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()
@ -120,10 +132,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

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

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

@ -448,7 +448,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);
@ -563,6 +563,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--------------------------------------------
@ -577,24 +588,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)) {

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

@ -49,6 +49,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,6 +62,8 @@ 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";
@ -104,13 +108,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)
@ -173,6 +176,8 @@ public class BaseApplication extends Application {
DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics(); DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics();
dpWidth = displayMetrics.widthPixels / displayMetrics.density; dpWidth = displayMetrics.widthPixels / displayMetrics.density;
displayRelativeTime = settingsSharedPrefs.getBoolean(DISPLAY_RELATIVE_TIME, false);
} }
//Getters //Getters
@ -192,6 +197,9 @@ public class BaseApplication extends Application {
return dpWidth; return dpWidth;
} }
public boolean isDisplayRelativeTimeEnabled() {
return displayRelativeTime;
}
//--------------------Firebase-------------------- //--------------------Firebase--------------------

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

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

@ -47,7 +47,7 @@ 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");

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

@ -0,0 +1,232 @@
package gr.thmmy.mthmmy.utils;
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);
}
}
}

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

@ -0,0 +1,106 @@
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.d("Will attempt to convert %s to timestamp.", thmmyDateTime);
String originalDateTime = thmmyDateTime;
DateTimeZone dtz = getDtz();
//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.d("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.d("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.d("DateTime %s was converted to %s, or %s", originalDateTime, timestamp, dateTime.toString());
return timestamp;
}
public static String convertDateTime(String dateTime, boolean removeSeconds){
//Convert e.g. Today at 12:16:48 -> 12:16:48, but October 03, 2019, 16:40:18 remains as is
if (!dateTime.contains(","))
dateTime = dateTime.replaceAll(".+? ([0-9])", "$1");
//Remove seconds
if(removeSeconds)
dateTime = dateTime.replaceAll("(.+?)(:[0-5][0-9])($|\\s)", "$1$3");
return dateTime;
}
@VisibleForTesting
private static DateTimeZone getDtz(){
if(!BaseApplication.getInstance().getSessionManager().isLoggedIn())
return DateTimeZone.forID("Europe/Athens");
else
return DateTimeZone.getDefault();
}
}

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

@ -110,30 +110,43 @@
<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/privacy_policy_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/epl_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/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.utils.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"

2
app/src/main/res/layout/fragment_unread_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.utils.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"

3
app/src/main/res/values/attrs.xml

@ -3,4 +3,7 @@
<declare-styleable name="EditorView" > <declare-styleable name="EditorView" >
<attr name="hint" format="string" /> <attr name="hint" format="string" />
</declare-styleable> </declare-styleable>
<declare-styleable name="RelativeTimeTextView">
<attr name="reference_time" format="string" />
</declare-styleable>
</resources> </resources>

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

@ -94,6 +94,7 @@
<string name="libraries_text">mTHMMY uses the following open-source libraries:</string> <string name="libraries_text">mTHMMY uses the following open-source libraries:</string>
<string name="apache_v2_0_libraries"><u>Apache v2.0 License libraries</u></string> <string name="apache_v2_0_libraries"><u>Apache v2.0 License libraries</u></string>
<string name="the_mit_libraries"><u>The MIT License libraries</u></string> <string name="the_mit_libraries"><u>The MIT License libraries</u></string>
<string name="epl_libraries"><u>Eclipse Public License v1.0 libraries</u></string>
<string name="contact">Contact</string> <string name="contact">Contact</string>
<string name="contact_text">Do not hesitate to contact us for any matter either by email at thmmynolife@gmail.com, or by joining our discord server at https://discord.gg/CVt3yrn.</string> <string name="contact_text">Do not hesitate to contact us for any matter either by email at thmmynolife@gmail.com, or by joining our discord server at https://discord.gg/CVt3yrn.</string>
<string name="open_source">Open Source</string> <string name="open_source">Open Source</string>
@ -169,26 +170,38 @@
<string name="title_activity_settings">Settings</string> <string name="title_activity_settings">Settings</string>
<string name="pref_category_app">App</string> <string name="pref_category_app">App</string>
<string name="pref_app_main_default_tab_key">pref_app_main_default_tab_key</string>
<string name="pref_title_app_main_default_tab">Default home tab</string> <string name="pref_title_app_main_default_tab">Default home tab</string>
<string name="pref_summary_app_main_default_tab">Sets a home screen tab as default</string> <string name="pref_summary_app_main_default_tab">Sets a home screen tab as default</string>
<string name="pref_app_main_default_tab_dialog_title">Default home tab</string> <string name="pref_app_main_default_tab_dialog_title">Default home tab</string>
<string name="pref_app_display_relative_time_key">pref_app_display_relative_time_key</string>
<string name="pref_title_display_relative_time">Display relative time</string>
<string name="pref_summary_display_relative_time">Considering that you haven\'t set some weird custom time format</string>
<string name="pref_category_notifications">Notifications</string> <string name="pref_category_notifications">Notifications</string>
<string name="pref_notification_vibration_enable_key">pref_notification_vibration_enable_key</string>
<string name="pref_title_notification_vibration_enable">Vibration</string> <string name="pref_title_notification_vibration_enable">Vibration</string>
<string name="pref_notification_led_enable_key">pref_notification_led_enable_key</string>
<string name="pref_title_notification_led_enable">Notifications led</string> <string name="pref_title_notification_led_enable">Notifications led</string>
<string name="pref_summary_notification_led_enable">Enables/disables the notifications led (if the device has one)</string> <string name="pref_summary_notification_led_enable">Enables/disables the notifications led (if the device has one)</string>
<string name="pref_notifications_select_sound_key">pref_notifications_select_sound_key</string>
<string name="pref_title_notifications_sound">Notifications sound</string> <string name="pref_title_notifications_sound">Notifications sound</string>
<string name="pref_summary_notifications_sound">Sets your preferred notification sound</string> <string name="pref_summary_notifications_sound">Sets your preferred notification sound</string>
<string name="pref_category_posting">Posting</string> <string name="pref_category_posting">Posting</string>
<string name="pref_category_posting_key">pref_category_posting_key</string>
<string name="pref_posting_app_signature_enable_key">pref_posting_app_signature_enable_key</string>
<string name="pref_title_posting_app_signature_enable">App signature</string> <string name="pref_title_posting_app_signature_enable">App signature</string>
<string name="pref_summary_posting_app_signature_enable">Appends a \"sent from mTHMMY\" message to your posts</string> <string name="pref_summary_posting_app_signature_enable">Appends a \"sent from mTHMMY\" message to your posts</string>
<string name="pref_category_uploading">Uploading</string> <string name="pref_category_uploading">Uploading</string>
<string name="pref_category_uploading_key">pref_category_uploading_key</string>
<string name="pref_uploading_app_signature_enable_key">pref_uploading_app_signature_enable_key</string>
<string name="pref_title_uploading_app_signature_enable">App signature</string> <string name="pref_title_uploading_app_signature_enable">App signature</string>
<string name="pref_summary_uploading_app_signature_enable">Appends an \"uploaded from mTHMMY\" message to the descriptions of your uploads</string> <string name="pref_summary_uploading_app_signature_enable">Appends an \"uploaded from mTHMMY\" message to the descriptions of your uploads</string>
<string name="pref_category_privacy">Privacy</string> <string name="pref_category_privacy">Privacy</string>
<string name="pref_category_privacy_key">pref_category_privacy_key</string>
<string name="pref_privacy_crashlytics_enable_key">pref_privacy_crashlytics_enable_key</string> <string name="pref_privacy_crashlytics_enable_key">pref_privacy_crashlytics_enable_key</string>
<string name="pref_title_privacy_crashlytics_enable">Crash data reports</string> <string name="pref_title_privacy_crashlytics_enable">Crash data reports</string>
<string name="pref_summary_privacy_crashlytics_enable">Automatically send us anonymized reports of errors and crashes</string> <string name="pref_summary_privacy_crashlytics_enable">Automatically send us anonymized reports of errors and crashes</string>
@ -219,6 +232,7 @@
<!-- New topic activity --> <!-- New topic activity -->
<string name="new_topic_toolbar">New topic</string> <string name="new_topic_toolbar">New topic</string>
<string name="create_topic">Create topic</string> <string name="create_topic">Create topic</string>
<string name="url_copied_msg">URL copied</string>
<!-- Inbox activity --> <!-- Inbox activity -->
<string name="personal_message_thumbnail">Personal message author thumbnail</string> <string name="personal_message_thumbnail">Personal message author thumbnail</string>

10
app/src/main/res/xml-v26/app_preferences_guest.xml

@ -10,14 +10,20 @@
android:dialogTitle="@string/pref_app_main_default_tab_dialog_title" android:dialogTitle="@string/pref_app_main_default_tab_dialog_title"
android:entries="@array/pref_app_main_default_tab_entries" android:entries="@array/pref_app_main_default_tab_entries"
android:entryValues="@array/pref_app_main_default_tab_values" android:entryValues="@array/pref_app_main_default_tab_values"
android:key="pref_app_main_default_tab_key" android:key="@string/pref_app_main_default_tab_key"
android:title="@string/pref_title_app_main_default_tab" android:title="@string/pref_title_app_main_default_tab"
android:summary="@string/pref_summary_app_main_default_tab" android:summary="@string/pref_summary_app_main_default_tab"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/pref_app_display_relative_time_key"
android:title="@string/pref_title_display_relative_time"
android:summary="@string/pref_summary_display_relative_time"
app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_privacy_key" android:key="@string/pref_category_privacy_key"
android:title="@string/pref_category_privacy" android:title="@string/pref_category_privacy"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat

18
app/src/main/res/xml-v26/app_preferences_user.xml

@ -10,38 +10,44 @@
android:dialogTitle="@string/pref_app_main_default_tab_dialog_title" android:dialogTitle="@string/pref_app_main_default_tab_dialog_title"
android:entries="@array/pref_app_main_default_tab_entries" android:entries="@array/pref_app_main_default_tab_entries"
android:entryValues="@array/pref_app_main_default_tab_values" android:entryValues="@array/pref_app_main_default_tab_values"
android:key="pref_app_main_default_tab_key" android:key="@string/pref_app_main_default_tab_key"
android:title="@string/pref_title_app_main_default_tab" android:title="@string/pref_title_app_main_default_tab"
android:summary="@string/pref_summary_app_main_default_tab" android:summary="@string/pref_summary_app_main_default_tab"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/pref_app_display_relative_time_key"
android:title="@string/pref_title_display_relative_time"
android:summary="@string/pref_summary_display_relative_time"
app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_posting_key" android:key="@string/pref_category_posting_key"
android:title="@string/pref_category_posting" android:title="@string/pref_category_posting"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_posting_app_signature_enable_key" android:key="@string/pref_posting_app_signature_enable_key"
android:title="@string/pref_title_posting_app_signature_enable" android:title="@string/pref_title_posting_app_signature_enable"
android:summary="@string/pref_summary_posting_app_signature_enable" android:summary="@string/pref_summary_posting_app_signature_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_uploading_key" android:key="@string/pref_category_uploading_key"
android:title="@string/pref_category_uploading" android:title="@string/pref_category_uploading"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_uploading_app_signature_enable_key" android:key="@string/pref_uploading_app_signature_enable_key"
android:title="@string/pref_title_uploading_app_signature_enable" android:title="@string/pref_title_uploading_app_signature_enable"
android:summary="@string/pref_summary_uploading_app_signature_enable" android:summary="@string/pref_summary_uploading_app_signature_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_privacy_key" android:key="@string/pref_category_privacy_key"
android:title="@string/pref_category_privacy" android:title="@string/pref_category_privacy"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat

16
app/src/main/res/xml/app_preferences_guest.xml

@ -10,10 +10,16 @@
android:dialogTitle="@string/pref_app_main_default_tab_dialog_title" android:dialogTitle="@string/pref_app_main_default_tab_dialog_title"
android:entries="@array/pref_app_main_default_tab_entries" android:entries="@array/pref_app_main_default_tab_entries"
android:entryValues="@array/pref_app_main_default_tab_values" android:entryValues="@array/pref_app_main_default_tab_values"
android:key="pref_app_main_default_tab_key" android:key="@string/pref_app_main_default_tab_key"
android:title="@string/pref_title_app_main_default_tab" android:title="@string/pref_title_app_main_default_tab"
android:summary="@string/pref_summary_app_main_default_tab" android:summary="@string/pref_summary_app_main_default_tab"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/pref_app_display_relative_time_key"
android:title="@string/pref_title_display_relative_time"
android:summary="@string/pref_summary_display_relative_time"
app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
@ -21,24 +27,24 @@
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_notification_vibration_enable_key" android:key="@string/pref_notification_vibration_enable_key"
android:title="@string/pref_title_notification_vibration_enable" android:title="@string/pref_title_notification_vibration_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_notification_led_enable_key" android:key="@string/pref_notification_led_enable_key"
android:title="@string/pref_title_notification_led_enable" android:title="@string/pref_title_notification_led_enable"
android:summary="@string/pref_summary_notification_led_enable" android:summary="@string/pref_summary_notification_led_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference <Preference
android:key="pref_notifications_select_sound_key" android:key="@string/pref_notifications_select_sound_key"
android:title="@string/pref_title_notifications_sound" android:title="@string/pref_title_notifications_sound"
android:summary="@string/pref_summary_notifications_sound" android:summary="@string/pref_summary_notifications_sound"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_privacy_key" android:key="@string/pref_category_privacy_key"
android:title="@string/pref_category_privacy" android:title="@string/pref_category_privacy"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat

24
app/src/main/res/xml/app_preferences_user.xml

@ -10,10 +10,16 @@
android:dialogTitle="@string/pref_app_main_default_tab_dialog_title" android:dialogTitle="@string/pref_app_main_default_tab_dialog_title"
android:entries="@array/pref_app_main_default_tab_entries" android:entries="@array/pref_app_main_default_tab_entries"
android:entryValues="@array/pref_app_main_default_tab_values" android:entryValues="@array/pref_app_main_default_tab_values"
android:key="pref_app_main_default_tab_key" android:key="@string/pref_app_main_default_tab_key"
android:title="@string/pref_title_app_main_default_tab" android:title="@string/pref_title_app_main_default_tab"
android:summary="@string/pref_summary_app_main_default_tab" android:summary="@string/pref_summary_app_main_default_tab"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/pref_app_display_relative_time_key"
android:title="@string/pref_title_display_relative_time"
android:summary="@string/pref_summary_display_relative_time"
app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
@ -21,48 +27,48 @@
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_notification_vibration_enable_key" android:key="@string/pref_notification_vibration_enable_key"
android:title="@string/pref_title_notification_vibration_enable" android:title="@string/pref_title_notification_vibration_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_notification_led_enable_key" android:key="@string/pref_notification_led_enable_key"
android:title="@string/pref_title_notification_led_enable" android:title="@string/pref_title_notification_led_enable"
android:summary="@string/pref_summary_notification_led_enable" android:summary="@string/pref_summary_notification_led_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference <Preference
android:key="pref_notifications_select_sound_key" android:key="@string/pref_notifications_select_sound_key"
android:title="@string/pref_title_notifications_sound" android:title="@string/pref_title_notifications_sound"
android:summary="@string/pref_summary_notifications_sound" android:summary="@string/pref_summary_notifications_sound"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_posting_key" android:key="@string/pref_category_posting_key"
android:title="@string/pref_category_posting" android:title="@string/pref_category_posting"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_posting_app_signature_enable_key" android:key="@string/pref_posting_app_signature_enable_key"
android:title="@string/pref_title_posting_app_signature_enable" android:title="@string/pref_title_posting_app_signature_enable"
android:summary="@string/pref_summary_posting_app_signature_enable" android:summary="@string/pref_summary_posting_app_signature_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_uploading_key" android:key="@string/pref_category_uploading_key"
android:title="@string/pref_category_uploading" android:title="@string/pref_category_uploading"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="pref_uploading_app_signature_enable_key" android:key="@string/pref_uploading_app_signature_enable_key"
android:title="@string/pref_title_uploading_app_signature_enable" android:title="@string/pref_title_uploading_app_signature_enable"
android:summary="@string/pref_summary_uploading_app_signature_enable" android:summary="@string/pref_summary_uploading_app_signature_enable"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</androidx.preference.PreferenceCategory> </androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory <androidx.preference.PreferenceCategory
android:key="pref_category_privacy_key" android:key="@string/pref_category_privacy_key"
android:title="@string/pref_category_privacy" android:title="@string/pref_category_privacy"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat

106
app/src/test/java/gr/thmmy/mthmmy/utils/DateTimeUtilsTest.java

@ -0,0 +1,106 @@
package gr.thmmy.mthmmy.utils;
import net.lachlanmckee.timberjunit.TimberTestRule;
import org.joda.time.DateTime;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static gr.thmmy.mthmmy.utils.DateTimeUtils.getRelativeTimeSpanString;
import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest(DateTimeUtils.class)
public class DateTimeUtilsTest {
@Rule
public TimberTestRule logAllAlwaysRule = TimberTestRule.logAllAlways();
private final long NOW = System.currentTimeMillis();
private final String [] expectedRelativeTimeSpans = {
"just now",
"just now",
"just now",
"1m",
"1m",
"1m",
"2m",
"3m",
"1h",
"1h 15m",
"2h",
"2h 20m",
"4h",
"20h",
"21h",
"21h",
"21h",
"22h",
"1d",
"1d",
"2d",
"2d",
"3d",
"16d",
"1 month",
"2 months",
"1 year",
"1 year",
"2 years",
"a long time ago"
};
private final long [] times = {
NOW,
newDT().minusSeconds(44).getMillis(),
newDT().minusSeconds(44).minusMillis(500).getMillis(),
newDT().minusSeconds(45).getMillis(),
newDT().minusSeconds(89).getMillis(),
newDT().minusSeconds(89).minusMillis(500).getMillis(),
newDT().minusSeconds(90).getMillis(),
newDT().minusMinutes(3).minusSeconds(10).getMillis(),
newDT().minusHours(1).minusMinutes(4).getMillis(),
newDT().minusHours(1).minusMinutes(15).getMillis(),
newDT().minusHours(2).minusMinutes(4).getMillis(),
newDT().minusHours(2).minusMinutes(20).getMillis(),
newDT().minusHours(3).minusMinutes(51).getMillis(),
newDT().minusHours(20).minusMinutes(10).getMillis(),
newDT().minusHours(20).minusMinutes(30).getMillis(),
newDT().minusHours(21).getMillis(),
newDT().minusHours(21).minusMinutes(29).getMillis(),
newDT().minusHours(21).minusMinutes(30).getMillis(),
newDT().minusHours(22).minusMinutes(30).getMillis(),
newDT().minusHours(34).getMillis(),
newDT().minusHours(38).getMillis(),
newDT().minusDays(2).minusHours(10).getMillis(),
newDT().minusDays(2).minusHours(17).getMillis(),
newDT().minusDays(16).getMillis(),
newDT().minusDays(30+12).getMillis(),
newDT().minusDays(2*30+14).getMillis(),
newDT().minusDays(14*30).getMillis(),
newDT().minusMonths(15).getMillis(),
newDT().minusMonths(22).getMillis(),
newDT().minusYears(22).getMillis()
};
private DateTime newDT(){
return new DateTime(NOW);
}
@Test
public void relativeTimeSpansAreConvertedCorrectly() {
PowerMockito.mockStatic(System.class);
when(System.currentTimeMillis()).thenReturn(NOW);
String[] timeStrings = new String[times.length];
for(int i=0; i<times.length; i++)
timeStrings[i] = getRelativeTimeSpanString(times[i]);
assertArrayEquals(expectedRelativeTimeSpans,timeStrings);
}
}

85
app/src/test/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParserTest.java

@ -0,0 +1,85 @@
package gr.thmmy.mthmmy.utils.parsing;
import net.lachlanmckee.timberjunit.TimberTestRule;
import org.joda.time.DateTimeZone;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotNull;
import static org.powermock.api.support.membermodification.MemberMatcher.method;
import static org.powermock.api.support.membermodification.MemberModifier.stub;
@RunWith(PowerMockRunner.class)
@PrepareForTest(ThmmyDateTimeParser.class)
public class ThmmyDateTimeParserTest {
@Rule
public TimberTestRule logAllAlwaysRule = TimberTestRule.logAllAlways();
private static final String TIME_ZONE ="Europe/Athens";
private static final String GET_DTZ ="getDtz";
private final String [] expTimestamps={"1569245936000","1569191627000","1570050809000"};
private final String [][] dateTimes = {
{
"Σεπτεμβρίου 23, 2019, 16:38:56",
"Σεπτεμβρίου 23, 2019, 16:38:56 pm",
"Σεπτεμβρίου 23, 2019, 04:38:56 pm",
"23 Σεπτεμβρίου 2019, 16:38:56",
"2019-09-23, 16:38:56",
"23-09-2019, 16:38:56"
},
{
"Σεπτεμβρίου 23, 2019, 01:33:47",
"Σεπτεμβρίου 23, 2019, 01:33:47 am",
"23 Σεπτεμβρίου 2019, 01:33:47",
"23-09-2019, 01:33:47",
"2019-09-23, 01:33:47"
},
{
"Οκτωβρίου 03, 2019, 12:13:29 am",
"Οκτωβρίου 03, 2019, 00:13:29 am"
}
};
@Test
public void dateTimesAreConvertedCorrectly() {
stub(method(ThmmyDateTimeParser.class, GET_DTZ)).toReturn(DateTimeZone.forID(TIME_ZONE));
String[][] expectedTimeStamps = new String[dateTimes.length][];
String[][] timeStamps = new String[dateTimes.length][];
for(int i=0; i<dateTimes.length; i++){
timeStamps[i] = new String[dateTimes[i].length];
expectedTimeStamps[i] = new String[dateTimes[i].length];
for(int j=0; j<dateTimes[i].length; j++){
expectedTimeStamps[i][j]=expTimestamps[i];
timeStamps[i][j]=convertToTimestamp(dateTimes[i][j]);
}
}
assertArrayEquals(expectedTimeStamps,timeStamps);
}
private final String [] todayDateTimes = {
"10:10:10",
"00:58:07",
"23:23:23",
"09:09:09 am",
"09:09:09 pm"
};
@Test
public void todayDateTimeConvertToNonNull() {
stub(method(ThmmyDateTimeParser.class, GET_DTZ)).toReturn(DateTimeZone.forID(TIME_ZONE));
for (String todayDateTime : todayDateTimes)
assertNotNull(convertToTimestamp(todayDateTime));
}
}

8
build.gradle

@ -3,13 +3,17 @@ apply plugin: "com.github.ben-manes.versions"
buildscript { buildscript {
repositories { repositories {
maven { url "https://jitpack.io" }
maven { url "https://maven.fabric.io/public" } maven { url "https://maven.fabric.io/public" }
google() google()
jcenter() jcenter()
maven { url "https://jitpack.io" }
} }
dependencies { dependencies {
<<<<<<< HEAD
classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.android.tools.build:gradle:3.5.3'
=======
classpath 'com.android.tools.build:gradle:3.5.1'
>>>>>>> develop
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
classpath 'io.fabric.tools:gradle:1.29.0' classpath 'io.fabric.tools:gradle:1.29.0'
classpath 'org.ajoberstar.grgit:grgit-core:3.1.1' // Also change in app/gradle/grgit.gradle classpath 'org.ajoberstar.grgit:grgit-core:3.1.1' // Also change in app/gradle/grgit.gradle
@ -19,9 +23,9 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
maven { url "https://jitpack.io" }
maven { url "https://maven.google.com" } maven { url "https://maven.google.com" }
jcenter() jcenter()
maven { url "https://jitpack.io" }
} }
} }

1
emojis/.gitignore

@ -0,0 +1 @@
/build

29
emojis/build.gradle

@ -0,0 +1,29 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 19
targetSdkVersion 29
versionCode 1
versionName "1.0"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
}

0
emojis/consumer-rules.pro

21
emojis/proguard-rules.pro

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

2
emojis/src/main/AndroidManifest.xml

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gr.thmmy.emojis" />

0
app/src/main/res/drawable/emoji_a_eatpaper.xml → emojis/src/main/res/drawable/emoji_a_eatpaper.xml

0
app/src/main/res/drawable/emoji_a_eatpaper_f0.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f0.png

Before

Width:  |  Height:  |  Size: 636 B

After

Width:  |  Height:  |  Size: 636 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f1.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f1.png

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f10.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f10.png

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 618 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f11.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f11.png

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 630 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f12.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f12.png

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f13.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f13.png

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 630 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f14.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f14.png

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f15.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f15.png

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 630 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f16.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f16.png

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f17.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f17.png

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f18.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f18.png

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f19.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f19.png

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f2.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f2.png

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 617 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f20.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f20.png

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f21.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f21.png

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f22.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f22.png

Before

Width:  |  Height:  |  Size: 633 B

After

Width:  |  Height:  |  Size: 633 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f23.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f23.png

Before

Width:  |  Height:  |  Size: 570 B

After

Width:  |  Height:  |  Size: 570 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f24.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f24.png

Before

Width:  |  Height:  |  Size: 614 B

After

Width:  |  Height:  |  Size: 614 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f25.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f25.png

Before

Width:  |  Height:  |  Size: 600 B

After

Width:  |  Height:  |  Size: 600 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f26.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f26.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f27.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f27.png

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 641 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f28.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f28.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f29.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f29.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f3.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f3.png

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f30.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f30.png

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 631 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f31.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f31.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f32.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f32.png

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 631 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f33.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f33.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f34.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f34.png

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 631 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f35.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f35.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f36.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f36.png

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 631 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f37.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f37.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f38.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f38.png

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 631 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f39.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f39.png

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f4.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f4.png

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 617 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f40.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f40.png

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f41.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f41.png

Before

Width:  |  Height:  |  Size: 620 B

After

Width:  |  Height:  |  Size: 620 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f42.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f42.png

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 565 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f43.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f43.png

Before

Width:  |  Height:  |  Size: 555 B

After

Width:  |  Height:  |  Size: 555 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f44.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f44.png

Before

Width:  |  Height:  |  Size: 622 B

After

Width:  |  Height:  |  Size: 622 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f45.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f45.png

Before

Width:  |  Height:  |  Size: 604 B

After

Width:  |  Height:  |  Size: 604 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f46.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f46.png

Before

Width:  |  Height:  |  Size: 573 B

After

Width:  |  Height:  |  Size: 573 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f47.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f47.png

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 605 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f48.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f48.png

Before

Width:  |  Height:  |  Size: 536 B

After

Width:  |  Height:  |  Size: 536 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f49.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f49.png

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f5.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f5.png

Before

Width:  |  Height:  |  Size: 614 B

After

Width:  |  Height:  |  Size: 614 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f50.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f50.png

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 527 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f51.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f51.png

Before

Width:  |  Height:  |  Size: 516 B

After

Width:  |  Height:  |  Size: 516 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f52.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f52.png

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 526 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f53.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f53.png

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f54.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f54.png

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f55.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f55.png

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 465 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f56.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f56.png

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 466 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f57.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f57.png

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 534 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f58.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f58.png

Before

Width:  |  Height:  |  Size: 521 B

After

Width:  |  Height:  |  Size: 521 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f59.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f59.png

Before

Width:  |  Height:  |  Size: 525 B

After

Width:  |  Height:  |  Size: 525 B

0
app/src/main/res/drawable/emoji_a_eatpaper_f6.png → emojis/src/main/res/drawable/emoji_a_eatpaper_f6.png

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 618 B

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

Loading…
Cancel
Save