Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
FreshRSS-Android
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
30
Issues
30
List
Boards
Labels
Service Desk
Milestones
Merge Requests
2
Merge Requests
2
Requirements
Requirements
List
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Operations
Operations
Environments
Packages & Registries
Packages & Registries
Package Registry
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issue
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Christophe Henry
FreshRSS-Android
Commits
a5f43c30
Commit
a5f43c30
authored
Feb 15, 2019
by
Christophe Henry
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add refresh feature
parent
8bcbd87e
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
165 additions
and
19 deletions
+165
-19
app/build.gradle
app/build.gradle
+14
-5
app/src/main/java/fr/chenry/android/freshrss/FreshRSSApplication.kt
...in/java/fr/chenry/android/freshrss/FreshRSSApplication.kt
+13
-0
app/src/main/java/fr/chenry/android/freshrss/activities/MainActivity.kt
...ava/fr/chenry/android/freshrss/activities/MainActivity.kt
+1
-6
app/src/main/java/fr/chenry/android/freshrss/components/subscriptionarticles/SubscriptionArticlesDetailFragment.kt
...ubscriptionarticles/SubscriptionArticlesDetailFragment.kt
+1
-1
app/src/main/java/fr/chenry/android/freshrss/components/subscriptions/MainSubscriptionFragment.kt
...hrss/components/subscriptions/MainSubscriptionFragment.kt
+15
-0
app/src/main/java/fr/chenry/android/freshrss/store/Store.kt
app/src/main/java/fr/chenry/android/freshrss/store/Store.kt
+25
-2
app/src/main/java/fr/chenry/android/freshrss/store/api/Api.kt
...src/main/java/fr/chenry/android/freshrss/store/api/Api.kt
+1
-1
app/src/main/java/fr/chenry/android/freshrss/store/databindingsupport/viewmodels/UnreadArticlesViewModel.kt
.../databindingsupport/viewmodels/UnreadArticlesViewModel.kt
+0
-1
app/src/main/java/fr/chenry/android/freshrss/utils/NotificationHelper.kt
...va/fr/chenry/android/freshrss/utils/NotificationHelper.kt
+66
-0
app/src/main/res/drawable/ic_refresh_white_24dp.xml
app/src/main/res/drawable/ic_refresh_white_24dp.xml
+9
-0
app/src/main/res/drawable/ic_share_black_24dp.xml
app/src/main/res/drawable/ic_share_black_24dp.xml
+1
-1
app/src/main/res/drawable/ic_web_black_24dp.xml
app/src/main/res/drawable/ic_web_black_24dp.xml
+1
-1
app/src/main/res/menu/article_actionbar.xml
app/src/main/res/menu/article_actionbar.xml
+1
-1
app/src/main/res/menu/main_actionbar.xml
app/src/main/res/menu/main_actionbar.xml
+10
-0
app/src/main/res/values/strings.xml
app/src/main/res/values/strings.xml
+7
-0
No files found.
app/build.gradle
View file @
a5f43c30
...
...
@@ -42,20 +42,24 @@ dependencies {
}
implementation
fileTree
(
dir:
"libs"
,
include:
[
"*.jar"
])
// Kotlin stuff
implementation
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation
"org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// Compat libraries
implementation
"com.android.support:appcompat-v7:$android_support_version"
implementation
"com.android.support:preference-v7:$android_support_version"
implementation
"com.android.support:support-core-utils:$android_support_version"
implementation
"com.android.support:support-fragment:$android_support_version"
implementation
"com.android.support:support-compat:28.0.0"
// AndroidX layout
implementation
"androidx.appcompat:appcompat:1.0.0-beta01"
implementation
"androidx.core:core-ktx:1.1.0-alpha03"
implementation
"com.google.android.material:material:1.0.0-beta01"
implementation
"androidx.constraintlayout:constraintlayout:1.1.3"
implementation
"androidx.recyclerview:recyclerview:1.0.0"
testImplementation
"junit:junit:4.12"
androidTestImplementation
"androidx.test:runner:$test_runnner_version"
androidTestImplementation
"androidx.test.espresso:espresso-core:$espresso_version"
// ViewModel and LiveData
implementation
"androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
...
...
@@ -63,8 +67,7 @@ dependencies {
implementation
"androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
testImplementation
"androidx.arch.core:core-testing:$lifecycle_version"
// Room
implementation
"androidx.room:room-runtime:$room_version"
kapt
"androidx.room:room-compiler:$room_version"
implementation
"androidx.room:room-rxjava2:$room_version"
...
...
@@ -79,5 +82,11 @@ dependencies {
implementation
"nl.komponents.kovenant:kovenant:$promise_version"
implementation
"nl.komponents.kovenant:kovenant-android:$promise_version"
// Utils
implementation
"org.apache.commons:commons-text:1.4"
// Tests
testImplementation
"junit:junit:4.12"
androidTestImplementation
"androidx.test:runner:$test_runnner_version"
androidTestImplementation
"androidx.test.espresso:espresso-core:$espresso_version"
}
app/src/main/java/fr/chenry/android/freshrss/FreshRSSApplication.kt
View file @
a5f43c30
package
fr.chenry.android.freshrss
import
android.app.Application
import
android.app.NotificationManager
import
androidx.core.app.NotificationManagerCompat
import
fr.chenry.android.freshrss.store.Store
import
nl.komponents.kovenant.android.startKovenant
import
nl.komponents.kovenant.android.stopKovenant
...
...
@@ -9,6 +11,9 @@ import java.util.*
class
FreshRSSApplication
:
Application
()
{
override
fun
onCreate
()
{
super
.
onCreate
()
// Stupid hack because Android is still not able to provide the current application application globally
// even though it is effectively a singleton...
FreshRSSApplication
.
application
=
this
startKovenant
()
Store
.
debugMode
=
try
{
val
properties
=
Properties
()
...
...
@@ -21,4 +26,12 @@ class FreshRSSApplication: Application() {
super
.
onTerminate
()
stopKovenant
()
}
companion
object
{
lateinit
var
application
:
FreshRSSApplication
private
set
val
notificationManager
:
NotificationManagerCompat
get
()
=
NotificationManagerCompat
.
from
(
FreshRSSApplication
.
application
)
}
}
\ No newline at end of file
app/src/main/java/fr/chenry/android/freshrss/activities/MainActivity.kt
View file @
a5f43c30
...
...
@@ -17,12 +17,7 @@ class MainActivity: AppCompatActivity() {
private
val
deferred
=
deferred
<
Unit
,
Throwable
>()
init
{
Store
.
getSubscriptions
()
.
bind
{
Store
.
getUnreadCount
()}
.
bind
{
all
(
Store
.
subscriptions
.
keys
.
map
{
Store
.
getStreamContents
(
it
)},
cancelOthersOnError
=
false
)}
.
bind
{
Store
.
getUnreadItems
()}
.
successUi
{
deferred
.
resolve
()}
.
failUi
(
deferred
::
reject
)
Store
.
refresh
().
successUi
{
deferred
.
resolve
()}.
failUi
(
deferred
::
reject
)
}
override
fun
onCreate
(
savedInstanceState
:
Bundle
?)
{
...
...
app/src/main/java/fr/chenry/android/freshrss/components/subscriptionarticles/SubscriptionArticlesDetailFragment.kt
View file @
a5f43c30
...
...
@@ -37,7 +37,7 @@ class SubscriptionArticlesDetailFragment: Fragment() {
}
override
fun
onCreateOptionsMenu
(
menu
:
Menu
?,
inflater
:
MenuInflater
?)
{
inflater
?.
inflate
(
R
.
menu
.
actionbar
,
menu
)
inflater
?.
inflate
(
R
.
menu
.
a
rticle_a
ctionbar
,
menu
)
menu
?.
findItem
(
R
.
id
.
action_share
)
.
let
{
MenuItemCompat
.
getActionProvider
(
it
)
as
ShareActionProvider
}
.
setShareIntent
(
...
...
app/src/main/java/fr/chenry/android/freshrss/components/subscriptions/MainSubscriptionFragment.kt
View file @
a5f43c30
...
...
@@ -9,6 +9,7 @@ import fr.chenry.android.freshrss.R
import
fr.chenry.android.freshrss.store.Store
import
fr.chenry.android.freshrss.store.dao.common.Subscription
import
fr.chenry.android.freshrss.store.dao.common.Subscriptions
import
fr.chenry.android.freshrss.utils.getOrDefault
import
kotlinx.android.synthetic.main.fragment_main_subscription.*
import
kotlin.reflect.KClass
...
...
@@ -21,6 +22,11 @@ class MainSubscriptionFragment: Fragment() {
else ->
TODO
(
"Handle bad section"
)
}
override
fun
onCreate
(
savedInstanceState
:
Bundle
?)
{
super
.
onCreate
(
savedInstanceState
)
setHasOptionsMenu
(
true
)
}
override
fun
onCreateView
(
inflater
:
LayoutInflater
,
container
:
ViewGroup
?,
savedInstanceState
:
Bundle
?):
View
?
{
val
view
=
inflater
.
inflate
(
R
.
layout
.
fragment_main_subscription
,
container
,
false
)
...
...
@@ -55,6 +61,15 @@ class MainSubscriptionFragment: Fragment() {
true
}
}
override
fun
onCreateOptionsMenu
(
menu
:
Menu
?,
inflater
:
MenuInflater
?)
{
inflater
?.
inflate
(
R
.
menu
.
main_actionbar
,
menu
)
menu
?.
findItem
(
R
.
id
.
action_refresh
)
?.
setOnMenuItemClickListener
{
Store
.
refresh
().
getOrDefault
(
null
).
let
{
true
}
}
super
.
onCreateOptionsMenu
(
menu
,
inflater
)
}
}
abstract
class
SubscriptionsFragment
:
Fragment
()
{
...
...
app/src/main/java/fr/chenry/android/freshrss/store/Store.kt
View file @
a5f43c30
package
fr.chenry.android.freshrss.store
import
androidx.lifecycle.MutableLiveData
import
fr.chenry.android.freshrss.R
import
fr.chenry.android.freshrss.R.string
import
fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection
import
fr.chenry.android.freshrss.store.api.Api
import
fr.chenry.android.freshrss.store.dao.common.StreamId
...
...
@@ -8,10 +10,12 @@ import fr.chenry.android.freshrss.store.dao.common.Subscription
import
fr.chenry.android.freshrss.store.dao.store.Article
import
fr.chenry.android.freshrss.store.dao.store.ItemId
import
fr.chenry.android.freshrss.store.databindingsupport.GenericLiveData
import
fr.chenry.android.freshrss.utils.NotificationHelper
import
fr.chenry.android.freshrss.utils.NotificationHelper.NotificationChanels.REFRESH
import
fr.chenry.android.freshrss.utils.e
import
nl.komponents.kovenant.
Promise
import
nl.komponents.kovenant.
*
import
nl.komponents.kovenant.functional.bind
import
nl.komponents.kovenant.
then
import
nl.komponents.kovenant.
ui.*
object
Store
{
var
debugMode
=
false
...
...
@@ -22,11 +26,30 @@ object Store {
val
subscriptionsSection
=
MutableLiveData
<
SubscriptionSection
>().
apply
{
this
.
value
=
SubscriptionSection
.
ALL
}
val
tags
=
MutableLiveData
<
List
<
String
>>().
apply
{
this
.
value
=
listOf
()}
val
favorites
=
GenericLiveData
<
String
,
Subscription
>(
mutableMapOf
())
private
val
refreshingPromise
=
MutableLiveData
<
Promise
<
Unit
,
Exception
>>()
private
var
lastFetchTimestamp
=
0L
fun
login
(
instance
:
String
,
user
:
String
,
password
:
String
):
Promise
<
Unit
,
Exception
>
=
api
.
login
(
instance
,
user
,
password
)
bind
{
getToken
()}
fun
refresh
():
Promise
<
Unit
,
Exception
>
{
if
(
refreshingPromise
.
value
!=
null
)
return
refreshingPromise
.
value
!!
val
notificationId
=
NotificationHelper
.
post
(
REFRESH
,
R
.
string
.
notification_refresh_title
)
val
promise
=
getSubscriptions
()
.
bind
{
getUnreadCount
()}
.
bind
{
all
(
subscriptions
.
keys
.
map
{
getStreamContents
(
it
)},
cancelOthersOnError
=
false
)}
.
bind
{
getUnreadItems
()}
refreshingPromise
.
postValue
(
promise
)
promise
.
always
{
refreshingPromise
.
postValue
(
null
)}
.
successUi
{
NotificationHelper
.
cancel
(
notificationId
)}
.
failUi
{
NotificationHelper
.
update
(
REFRESH
,
notificationId
,
string
.
notification_refresh_failed_title
)}
return
promise
}
fun
getSubscriptions
():
Promise
<
Unit
,
Exception
>
=
api
.
getSubscriptions
()
then
{
subscriptions
.
addAll
(
it
.
map
{
self
->
self
.
id
to
self
})}
...
...
app/src/main/java/fr/chenry/android/freshrss/store/api/Api.kt
View file @
a5f43c30
...
...
@@ -71,7 +71,7 @@ class Api {
return
Fuel
.
getJson
(
endpoints
.
streamItemsEndpoint
,
listOf
(
"s"
to
id
))
.
authorize
(
this
.
authTokens
)
.
promiseString
()
successUi
{
this
.
e
(
it
)}
.
promiseString
()
successUi
{
this
.
e
(
"::getStreamItems: TODO"
)}
}
fun
getStreamContents
(
id
:
String
):
Promise
<
ContentItemsHandler
,
Exception
>
{
...
...
app/src/main/java/fr/chenry/android/freshrss/store/databindingsupport/viewmodels/UnreadArticlesViewModel.kt
View file @
a5f43c30
...
...
@@ -8,7 +8,6 @@ class UnreadArticlesViewModel: ArticlesViewModel() {
override
val
articles
:
Articles
get
()
=
liveData
.
value
?:
listOf
()
override
fun
load
():
Articles
{
this
.
e
(
this
::
class
.
qualifiedName
!!
)
val
subscription
=
Store
.
subscriptions
[
streamId
.
value
!!
]
if
(
subscription
==
null
)
{
this
.
w
(
"Unable to find unread articles for stream id ${streamId.value}"
)
...
...
app/src/main/java/fr/chenry/android/freshrss/utils/NotificationHelper.kt
0 → 100644
View file @
a5f43c30
package
fr.chenry.android.freshrss.utils
import
android.app.*
import
android.content.Context
import
android.os.Build
import
androidx.core.app.NotificationCompat
import
androidx.core.app.NotificationManagerCompat
import
fr.chenry.android.freshrss.FreshRSSApplication
import
fr.chenry.android.freshrss.R
object
NotificationHelper
{
fun
post
(
channel
:
NotificationChanels
,
contentTitleId
:
Int
,
notificationId
:
Int
=
channel
.
channelId
):
Int
{
NotificationManagerCompat
.
from
(
FreshRSSApplication
.
application
)
.
notify
(
notificationId
,
getNotification
(
channel
,
contentTitleId
))
return
notificationId
}
fun
update
(
channel
:
NotificationChanels
,
notificationId
:
Int
,
contentTitleId
:
Int
)
=
FreshRSSApplication
.
notificationManager
.
notify
(
notificationId
,
getNotification
(
channel
,
contentTitleId
))
private
fun
getNotification
(
channel
:
NotificationChanels
,
contentTitleId
:
Int
):
Notification
=
NotificationCompat
.
Builder
(
FreshRSSApplication
.
application
,
channel
.
channelTitle
)
.
setSmallIcon
(
R
.
drawable
.
ic_rss_feed_black_24dp
)
.
setContentTitle
(
FreshRSSApplication
.
application
.
resources
.
getString
(
contentTitleId
))
.
setPriority
(
NotificationCompat
.
PRIORITY_DEFAULT
)
.
build
()
fun
cancel
(
id
:
Int
)
=
NotificationManagerCompat
.
from
(
FreshRSSApplication
.
application
).
cancel
(
id
)
private
fun
createNotificationChannel
(
channelNameId
:
Int
,
channelDescription
:
Int
):
Pair
<
Int
,
String
>
{
if
(
Build
.
VERSION
.
SDK_INT
<=
Build
.
VERSION_CODES
.
O
)
return
0
to
""
return
try
{
val
name
=
FreshRSSApplication
.
application
.
resources
.
getString
(
channelNameId
)
val
descriptionText
=
FreshRSSApplication
.
application
.
resources
.
getString
(
channelDescription
)
val
importance
=
NotificationManager
.
IMPORTANCE_DEFAULT
val
channelId
=
FreshRSSApplication
.
application
.
resources
.
getString
(
R
.
string
.
notification_channel_refresh
)
val
channel
=
NotificationChannel
(
channelId
,
name
,
importance
).
apply
{
description
=
descriptionText
}
// Register the channel with the system
val
notificationManager
=
FreshRSSApplication
.
application
.
getSystemService
(
Context
.
NOTIFICATION_SERVICE
)
as
NotificationManager
notificationManager
.
createNotificationChannel
(
channel
)
channelNameId
to
channelId
}
catch
(
_
:
Throwable
)
{
0
to
"DEFAULT"
}
}
enum
class
NotificationChanels
(
channel
:
Pair
<
Int
,
String
>)
{
REFRESH
(
createNotificationChannel
(
R
.
string
.
notification_refresh_title
,
R
.
string
.
notification_refresh_description
)
);
val
channelId
=
channel
.
first
val
channelTitle
=
channel
.
second
}
}
\ No newline at end of file
app/src/main/res/drawable/ic_refresh_white_24dp.xml
0 → 100644
View file @
a5f43c30
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:width=
"24dp"
android:height=
"24dp"
android:viewportWidth=
"24.0"
android:viewportHeight=
"24.0"
>
<path
android:fillColor=
"#FFF"
android:pathData=
"M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"
/>
</vector>
app/src/main/res/drawable/ic_share_black_24dp.xml
View file @
a5f43c30
...
...
@@ -4,6 +4,6 @@
android:viewportWidth=
"24.0"
android:viewportHeight=
"24.0"
>
<path
android:fillColor=
"#FF
000000
"
android:fillColor=
"#FF
F
"
android:pathData=
"M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"
/>
</vector>
app/src/main/res/drawable/ic_web_black_24dp.xml
View file @
a5f43c30
...
...
@@ -4,6 +4,6 @@
android:viewportWidth=
"24.0"
android:viewportHeight=
"24.0"
>
<path
android:fillColor=
"#FF
000000
"
android:fillColor=
"#FF
F
"
android:pathData=
"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"
/>
</vector>
app/src/main/res/menu/actionbar.xml
→
app/src/main/res/menu/a
rticle_a
ctionbar.xml
View file @
a5f43c30
...
...
@@ -7,7 +7,7 @@
android:icon=
"@drawable/ic_share_black_24dp"
android:title=
"@string/share"
app:showAsAction=
"ifRoom"
app:actionProviderClass=
"androidx.appcompat.widget.ShareActionProvider"
/>
app:actionProviderClass=
"androidx.appcompat.widget.ShareActionProvider"
/>
<item
android:id=
"@+id/action_origin"
android:orderInCategory=
"100"
...
...
app/src/main/res/menu/main_actionbar.xml
0 → 100644
View file @
a5f43c30
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:app=
"http://schemas.android.com/apk/res-auto"
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<item
android:id=
"@+id/action_refresh"
android:orderInCategory=
"100"
android:icon=
"@drawable/ic_refresh_white_24dp"
android:title=
"@string/refresh"
app:showAsAction=
"always"
/>
</menu>
\ No newline at end of file
app/src/main/res/values/strings.xml
View file @
a5f43c30
...
...
@@ -32,4 +32,11 @@
<string
name=
"next"
>
Next
</string>
<string
name=
"error_invalid_password"
>
This password is too short
</string>
<string
name=
"original_page"
>
Original page
</string>
<string
name=
"notification_refresh_title"
>
Refreshing your RSS feeds
</string>
<!-- Do not translate -->
<string
name=
"notification_channel_refresh"
>
REFRESH
</string>
<string
name=
"notification_refresh_description"
>
FreshRSS is fetching your content from the server
</string>
<string
name=
"notification_refresh_failed_title"
>
FresshRSS failed to retreive content from your FreshRSS server
</string>
<string
name=
"refresh"
>
Refresh
</string>
</resources>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment