diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 72aa387a..585ff833 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,7 +36,7 @@
+ android:configChanges="orientation|screenSize|keyboardHidden" />
@@ -105,7 +105,7 @@
+ android:windowSoftInputMode="stateVisible|adjustResize" />
@@ -145,6 +145,7 @@
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
+
+ tools:node="remove" />
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
index 3013aeeb..c91b20cb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
@@ -47,6 +47,7 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.appstore.*
+import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
@@ -67,6 +68,9 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
+import com.mikepenz.materialdrawer.holder.BadgeStyle
+import com.mikepenz.materialdrawer.holder.ColorHolder
+import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.iconics.iconicsIcon
import com.mikepenz.materialdrawer.model.*
import com.mikepenz.materialdrawer.model.interfaces.*
@@ -97,6 +101,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private var notificationTabPosition = 0
private var onTabSelectedListener: OnTabSelectedListener? = null
+ private var unreadAnnouncementsCount = 0
+
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
private val emojiInitCallback = object : InitCallback() {
@@ -191,6 +197,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
* drawer, though, because its callback touches the header in the drawer. */
fetchUserInfo()
+ fetchAnnouncements()
+
setupTabs(showNotificationTab)
// Setup push notifications
@@ -206,6 +214,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
when (event) {
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
is MainTabsChangedEvent -> setupTabs(false)
+ is AnnouncementReadEvent -> {
+ unreadAnnouncementsCount--
+ updateAnnouncementsBadge()
+ }
}
}
@@ -392,6 +404,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
}
},
+ primaryDrawerItem {
+ identifier = DRAWER_ITEM_ANNOUNCEMENTS
+ nameRes = R.string.title_announcements
+ iconRes = R.drawable.ic_bullhorn_24dp
+ onClick = {
+ startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
+ }
+ badgeStyle = BadgeStyle().apply {
+ textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
+ color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
+ }
+ },
DividerDrawerItem(),
secondaryDrawerItem {
nameRes = R.string.action_view_account_preferences
@@ -653,6 +677,25 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
updateShortcut(this, accountManager.activeAccount!!)
}
+ private fun fetchAnnouncements() {
+ mastodonApi.listAnnouncements(false)
+ .observeOn(AndroidSchedulers.mainThread())
+ .autoDispose(this, Lifecycle.Event.ON_DESTROY)
+ .subscribe(
+ { announcements ->
+ unreadAnnouncementsCount = announcements.count { !it.read }
+ updateAnnouncementsBadge()
+ },
+ {
+ Log.w(TAG, "Failed to fetch announcements.", it)
+ }
+ )
+ }
+
+ private fun updateAnnouncementsBadge() {
+ mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount == 0) null else unreadAnnouncementsCount.toString()))
+ }
+
private fun updateProfiles() {
val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header))
@@ -687,6 +730,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10
+ private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
const val STATUS_URL = "statusUrl"
}
}
@@ -716,4 +760,4 @@ private var AbstractDrawerItem<*, *>.onClick: () -> Unit
value()
false
}
- }
\ No newline at end of file
+ }
diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
index 8e62faeb..288de430 100644
--- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
@@ -19,4 +19,5 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
data class MainTabsChangedEvent(val newTabs: List) : Dispatchable
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
-data class DomainMuteEvent(val instance: String): Dispatchable
\ No newline at end of file
+data class DomainMuteEvent(val instance: String): Dispatchable
+data class AnnouncementReadEvent(val announcementId: String): Dispatchable
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
new file mode 100644
index 00000000..c4fa93f2
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
@@ -0,0 +1,115 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.announcements
+
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.view.size
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipGroup
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.entity.Announcement
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.util.emojify
+import kotlinx.android.synthetic.main.item_announcement.view.*
+
+interface AnnouncementActionListener {
+ fun openReactionPicker(announcementId: String, target: View)
+ fun addReaction(announcementId: String, name: String)
+ fun removeReaction(announcementId: String, name: String)
+}
+
+class AnnouncementAdapter(
+ private var items: List = emptyList(),
+ private val listener: AnnouncementActionListener
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_announcement, parent, false)
+ return AnnouncementViewHolder(view)
+ }
+
+ override fun onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) {
+ viewHolder.bind(items[position])
+ }
+
+ override fun getItemCount() = items.size
+
+ fun updateList(items: List) {
+ this.items = items
+ notifyDataSetChanged()
+ }
+
+ inner class AnnouncementViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
+
+ private val text: TextView = view.text
+ private val chips: ChipGroup = view.chipGroup
+ private val addReactionChip: Chip = view.addReactionChip
+
+ fun bind(item: Announcement) {
+ text.text = item.content
+
+ item.reactions.forEachIndexed { i, reaction ->
+ (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
+ ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
+ isCheckable = true
+ checkedIcon = null
+ chips.addView(this, i)
+ })
+ .apply {
+ val emojiText = if (reaction.url == null) {
+ reaction.name
+ } else {
+ view.context.getString(R.string.emoji_shortcode_format, reaction.name)
+ }
+ text = ("$emojiText ${reaction.count}")
+ .emojify(
+ listOf(Emoji(
+ reaction.name,
+ reaction.url ?: "",
+ reaction.staticUrl ?: "",
+ null
+ )),
+ this
+ )
+
+ isChecked = reaction.me
+
+ setOnClickListener {
+ if (reaction.me) {
+ listener.removeReaction(item.id, reaction.name)
+ } else {
+ listener.addReaction(item.id, reaction.name)
+ }
+ }
+ }
+ }
+
+ while (chips.size - 1 > item.reactions.size) {
+ chips.removeViewAt(item.reactions.size)
+ }
+
+ addReactionChip.setOnClickListener {
+ listener.openReactionPicker(item.id, it)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt
new file mode 100644
index 00000000..f9c0ae72
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt
@@ -0,0 +1,153 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.announcements
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.widget.PopupWindow
+import androidx.activity.viewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.keylesspalace.tusky.BaseActivity
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.adapter.EmojiAdapter
+import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.util.*
+import com.keylesspalace.tusky.view.EmojiPicker
+import kotlinx.android.synthetic.main.activity_announcements.*
+import kotlinx.android.synthetic.main.toolbar_basic.*
+import javax.inject.Inject
+
+class AnnouncementsActivity : BaseActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable {
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
+
+ private val adapter = AnnouncementAdapter(emptyList(), this)
+
+ private val picker by lazy { EmojiPicker(this) }
+ private val pickerDialog by lazy {
+ PopupWindow(this)
+ .apply {
+ contentView = picker
+ isFocusable = true
+ setOnDismissListener {
+ currentAnnouncementId = null
+ }
+ }
+ }
+ private var currentAnnouncementId: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_announcements)
+
+ setSupportActionBar(toolbar)
+ supportActionBar?.apply {
+ title = getString(R.string.title_announcements)
+ setDisplayHomeAsUpEnabled(true)
+ setDisplayShowHomeEnabled(true)
+ }
+
+ swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements)
+ swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
+
+ announcementsList.setHasFixedSize(true)
+ announcementsList.layoutManager = LinearLayoutManager(this)
+ val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
+ announcementsList.addItemDecoration(divider)
+ announcementsList.adapter = adapter
+
+ viewModel.announcements.observe(this, Observer {
+ when (it) {
+ is Success -> {
+ progressBar.hide()
+ swipeRefreshLayout.isRefreshing = false
+ if (it.data.isNullOrEmpty()) {
+ errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
+ errorMessageView.show()
+ } else {
+ errorMessageView.hide()
+ }
+ adapter.updateList(it.data ?: listOf())
+ }
+ is Loading -> {
+ errorMessageView.hide()
+ }
+ is Error -> {
+ progressBar.hide()
+ swipeRefreshLayout.isRefreshing = false
+ errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
+ refreshAnnouncements()
+ }
+ errorMessageView.show()
+ }
+ }
+ })
+
+ viewModel.emojis.observe(this, Observer {
+ picker.adapter = EmojiAdapter(it, this)
+ })
+
+ viewModel.load()
+ progressBar.show()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun refreshAnnouncements() {
+ viewModel.load()
+ swipeRefreshLayout.isRefreshing = true
+ }
+
+ override fun openReactionPicker(announcementId: String, target: View) {
+ currentAnnouncementId = announcementId
+ pickerDialog.showAsDropDown(target)
+ }
+
+ override fun onEmojiSelected(shortcode: String) {
+ viewModel.addReaction(currentAnnouncementId!!, shortcode)
+ pickerDialog.dismiss()
+ }
+
+ override fun addReaction(announcementId: String, name: String) {
+ viewModel.addReaction(announcementId, name)
+ }
+
+ override fun removeReaction(announcementId: String, name: String) {
+ viewModel.removeReaction(announcementId, name)
+ }
+
+ companion object {
+ fun newIntent(context: Context) = Intent(context, AnnouncementsActivity::class.java)
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
new file mode 100644
index 00000000..2fd1fbae
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
@@ -0,0 +1,187 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.announcements
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
+import com.keylesspalace.tusky.appstore.EventHub
+import com.keylesspalace.tusky.db.AccountManager
+import com.keylesspalace.tusky.db.AppDatabase
+import com.keylesspalace.tusky.db.InstanceEntity
+import com.keylesspalace.tusky.entity.Announcement
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.entity.Instance
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.*
+import io.reactivex.rxkotlin.Singles
+import javax.inject.Inject
+
+class AnnouncementsViewModel @Inject constructor(
+ accountManager: AccountManager,
+ private val appDatabase: AppDatabase,
+ private val mastodonApi: MastodonApi,
+ private val eventHub: EventHub
+) : RxAwareViewModel() {
+
+ private val announcementsMutable = MutableLiveData>>()
+ val announcements: LiveData>> = announcementsMutable
+
+ private val emojisMutable = MutableLiveData>()
+ val emojis: LiveData> = emojisMutable
+
+ init {
+ Singles.zip(
+ mastodonApi.getCustomEmojis(),
+ appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
+ .map> { Either.Left(it) }
+ .onErrorResumeNext(
+ mastodonApi.getInstance()
+ .map { Either.Right(it) }
+ )
+ ) { emojis, either ->
+ either.asLeftOrNull()?.copy(emojiList = emojis)
+ ?: InstanceEntity(
+ accountManager.activeAccount?.domain!!,
+ emojis,
+ either.asRight().maxTootChars,
+ either.asRight().pollLimits?.maxOptions,
+ either.asRight().pollLimits?.maxOptionChars,
+ either.asRight().version
+ )
+ }
+ .doOnSuccess {
+ appDatabase.instanceDao().insertOrReplace(it)
+ }
+ .subscribe({
+ emojisMutable.postValue(it.emojiList)
+ }, {
+ Log.w(TAG, "Failed to get custom emojis.", it)
+ })
+ .autoDispose()
+ }
+
+ fun load() {
+ announcementsMutable.postValue(Loading())
+ mastodonApi.listAnnouncements()
+ .subscribe({
+ announcementsMutable.postValue(Success(it))
+ it.filter { announcement -> !announcement.read }
+ .forEach { announcement ->
+ mastodonApi.dismissAnnouncement(announcement.id)
+ .subscribe(
+ {
+ eventHub.dispatch(AnnouncementReadEvent(announcement.id))
+ },
+ { throwable ->
+ Log.d(TAG, "Failed to mark announcement as read.", throwable)
+ }
+ )
+ .autoDispose()
+ }
+ }, {
+ announcementsMutable.postValue(Error(cause = it))
+ })
+ .autoDispose()
+ }
+
+ fun addReaction(announcementId: String, name: String) {
+ mastodonApi.addAnnouncementReaction(announcementId, name)
+ .subscribe({
+ announcementsMutable.postValue(
+ Success(
+ announcements.value!!.data!!.map { announcement ->
+ if (announcement.id == announcementId) {
+ announcement.copy(
+ reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
+ announcement.reactions.map { reaction ->
+ if (reaction.name == name) {
+ reaction.copy(
+ count = reaction.count + 1,
+ me = true
+ )
+ } else {
+ reaction
+ }
+ }
+ } else {
+ listOf(
+ *announcement.reactions.toTypedArray(),
+ emojis.value!!.find { emoji -> emoji.shortcode == name }
+ !!.run {
+ Announcement.Reaction(
+ name,
+ 1,
+ true,
+ url,
+ staticUrl
+ )
+ }
+ )
+ }
+ )
+ } else {
+ announcement
+ }
+ }
+ )
+ )
+ }, {
+ Log.w(TAG, "Failed to add reaction to the announcement.", it)
+ })
+ .autoDispose()
+ }
+
+ fun removeReaction(announcementId: String, name: String) {
+ mastodonApi.removeAnnouncementReaction(announcementId, name)
+ .subscribe({
+ announcementsMutable.postValue(
+ Success(
+ announcements.value!!.data!!.map { announcement ->
+ if (announcement.id == announcementId) {
+ announcement.copy(
+ reactions = announcement.reactions.mapNotNull { reaction ->
+ if (reaction.name == name) {
+ if (reaction.count > 1) {
+ reaction.copy(
+ count = reaction.count - 1,
+ me = false
+ )
+ } else {
+ null
+ }
+ } else {
+ reaction
+ }
+ }
+ )
+ } else {
+ announcement
+ }
+ }
+ )
+ )
+ }, {
+ Log.w(TAG, "Failed to remove reaction from the announcement.", it)
+ })
+ .autoDispose()
+ }
+
+ companion object {
+ private const val TAG = "AnnouncementsViewModel"
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
index 45f72a3e..4eba7e32 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
@@ -50,7 +50,6 @@ import androidx.core.view.inputmethod.InputContentInfoCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
-import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -301,7 +300,7 @@ class ComposeActivity : BaseActivity(),
}
viewModel.media.observe { media ->
mediaAdapter.submitList(media)
- if(media.size != mediaCount) {
+ if (media.size != mediaCount) {
mediaCount = media.size
composeMediaPreviewBar.visible(media.isNotEmpty())
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
@@ -311,8 +310,8 @@ class ComposeActivity : BaseActivity(),
pollPreview.visible(poll != null)
poll?.let(pollPreview::setPoll)
}
- viewModel.scheduledAt.observe {scheduledAt ->
- if(scheduledAt == null) {
+ viewModel.scheduledAt.observe { scheduledAt ->
+ if (scheduledAt == null) {
composeScheduleView.resetSchedule()
} else {
composeScheduleView.setDateTime(scheduledAt)
@@ -344,7 +343,6 @@ class ComposeActivity : BaseActivity(),
scheduleBehavior = BottomSheetBehavior.from(composeScheduleView)
emojiBehavior = BottomSheetBehavior.from(emojiView)
- emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false)
enableButton(composeEmojiButton, clickable = false, colorActive = false)
// Setup the interface buttons.
@@ -552,7 +550,7 @@ class ComposeActivity : BaseActivity(),
}
private fun onScheduleClick() {
- if(viewModel.scheduledAt.value == null) {
+ if (viewModel.scheduledAt.value == null) {
composeScheduleView.openPickDateDialog()
} else {
showScheduleView()
@@ -715,9 +713,9 @@ class ComposeActivity : BaseActivity(),
// Verify the returned content's type is of the correct MIME type
val supported = inputContentInfo.description.hasMimeType("image/*")
- if(supported) {
+ if (supported) {
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
- if(lacksPermission) {
+ if (lacksPermission) {
try {
inputContentInfo.requestPermission()
} catch (e: Exception) {
@@ -771,7 +769,7 @@ class ComposeActivity : BaseActivity(),
Snackbar.LENGTH_SHORT).apply {
}
- bar.setAction(R.string.action_retry) { onMediaPick()}
+ bar.setAction(R.string.action_retry) { onMediaPick() }
//necessary so snackbar is shown over everything
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
bar.show()
@@ -913,7 +911,7 @@ class ComposeActivity : BaseActivity(),
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
Log.d(TAG, event.toString())
- if(event.action == KeyEvent.ACTION_DOWN) {
+ if (event.action == KeyEvent.ACTION_DOWN) {
if (event.isCtrlPressed) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// send toot by pressing CTRL + ENTER
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
index ea2741ca..0257c28f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -16,6 +16,7 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.*
+import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.preference.PreferencesActivity
@@ -103,4 +104,7 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
+
+ @ContributesAndroidInjector
+ abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
index f4929463..c461012d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
@@ -4,6 +4,7 @@ package com.keylesspalace.tusky.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel
@@ -85,5 +86,10 @@ abstract class ViewModelModule {
@ViewModelKey(ScheduledTootViewModel::class)
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
+ @Binds
+ @IntoMap
+ @ViewModelKey(AnnouncementsViewModel::class)
+ internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
+
//Add more ViewModels here
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt
new file mode 100644
index 00000000..5cd32fe8
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt
@@ -0,0 +1,57 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.entity
+
+import android.text.Spanned
+import com.google.gson.annotations.SerializedName
+import java.util.*
+
+data class Announcement(
+ val id: String,
+ val content: Spanned,
+ @SerializedName("starts_at") val startsAt: Date?,
+ @SerializedName("ends_at") val endsAt: Date?,
+ @SerializedName("all_day") val allDay: Boolean,
+ @SerializedName("published_at") val publishedAt: Date,
+ @SerializedName("updated_at") val updatedAt: Date,
+ val read: Boolean,
+ val mentions: List,
+ val statuses: List,
+ val tags: List,
+ val emojis: List,
+ val reactions: List
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+
+ val announcement = other as Announcement?
+ return id == announcement?.id
+ }
+
+ override fun hashCode(): Int {
+ return id.hashCode()
+ }
+
+ data class Reaction(
+ val name: String,
+ var count: Int,
+ var me: Boolean,
+ val url: String?,
+ @SerializedName("static_url") val staticUrl: String?
+ )
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
index baee54bc..fe7a22c7 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
@@ -23,5 +23,6 @@ import kotlinx.android.parcel.Parcelize
data class Emoji(
val shortcode: String,
val url: String,
+ @SerializedName("static_url") val staticUrl: String,
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
-) : Parcelable
\ No newline at end of file
+) : Parcelable
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
index a43ac9b0..8f3dab3f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@@ -513,6 +513,28 @@ interface MastodonApi {
@Field("choices[]") choices: List
): Single
+ @GET("api/v1/announcements")
+ fun listAnnouncements(
+ @Query("with_dismissed") withDismissed: Boolean = true
+ ): Single>
+
+ @POST("api/v1/announcements/{id}/dismiss")
+ fun dismissAnnouncement(
+ @Path("id") announcementId: String
+ ): Single
+
+ @PUT("api/v1/announcements/{id}/reactions/{name}")
+ fun addAnnouncementReaction(
+ @Path("id") announcementId: String,
+ @Path("name") name: String
+ ): Single
+
+ @DELETE("api/v1/announcements/{id}/reactions/{name}")
+ fun removeAnnouncementReaction(
+ @Path("id") announcementId: String,
+ @Path("name") name: String
+ ): Single
+
@FormUrlEncoded
@POST("api/v1/reports")
fun reportObservable(
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt
new file mode 100644
index 00000000..09e648ad
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt
@@ -0,0 +1,17 @@
+package com.keylesspalace.tusky.view
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+class EmojiPicker @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : RecyclerView(context, attrs) {
+
+ init {
+ clipToPadding = false
+ layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false)
+ }
+}
diff --git a/app/src/main/res/drawable/ic_bullhorn_24dp.xml b/app/src/main/res/drawable/ic_bullhorn_24dp.xml
new file mode 100644
index 00000000..e290b24e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bullhorn_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_announcements.xml b/app/src/main/res/layout/activity_announcements.xml
new file mode 100644
index 00000000..c0504b83
--- /dev/null
+++ b/app/src/main/res/layout/activity_announcements.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml
index 355841d9..5f8d6ffd 100644
--- a/app/src/main/res/layout/activity_compose.xml
+++ b/app/src/main/res/layout/activity_compose.xml
@@ -193,14 +193,12 @@
android:textSize="?attr/status_text_medium" />
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 32602bfd..bc340a3d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -42,6 +42,7 @@
Edit your profile
Drafts
Scheduled toots
+ Announcements
Licenses
\@%s
@@ -569,6 +570,7 @@
You don\'t have any drafts.
You don\'t have any scheduled statuses.
+ There are no announcements.
Mastodon has a minimum scheduling interval of 5 minutes.
Show link previews in timelines
Show confirmation dialog before boosting
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 0c9c66f4..50da2622 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -79,6 +79,7 @@
- ?attr/colorSurface
+ - @style/Widget.MaterialComponents.Chip.Choice