[needs help] Support announcements (#1977)
* Implement announcements activity * Update reactions without api access * Add badge style * Use emptyList() as default parameter * Simplify newIntent * Use List instead of Array * Remove unneeded ConstraintLayout * Add lineSpacingMultiplier * Fix wording * Apply material design's default chip style * Dismiss announcements automatically
This commit is contained in:
parent
94271815eb
commit
fef4b8b07f
19 changed files with 717 additions and 21 deletions
|
@ -36,7 +36,7 @@
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".SavedTootActivity"
|
android:name=".SavedTootActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden"/>
|
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".LoginActivity"
|
android:name=".LoginActivity"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.compose.ComposeActivity"
|
android:name=".components.compose.ComposeActivity"
|
||||||
android:theme="@style/TuskyDialogActivityTheme"
|
android:theme="@style/TuskyDialogActivityTheme"
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize"/>
|
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewThreadActivity"
|
android:name=".ViewThreadActivity"
|
||||||
android:configChanges="orientation|screenSize" />
|
android:configChanges="orientation|screenSize" />
|
||||||
|
@ -145,6 +145,7 @@
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||||
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
||||||
|
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||||
<receiver
|
<receiver
|
||||||
|
@ -180,7 +181,7 @@
|
||||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||||
android:authorities="${applicationId}.workmanager-init"
|
android:authorities="${applicationId}.workmanager-init"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
tools:node="remove"/>
|
tools:node="remove" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -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.TabLayout.OnTabSelectedListener
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.keylesspalace.tusky.appstore.*
|
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
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
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.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
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.iconics.iconicsIcon
|
||||||
import com.mikepenz.materialdrawer.model.*
|
import com.mikepenz.materialdrawer.model.*
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||||
|
@ -97,6 +101,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
private var notificationTabPosition = 0
|
private var notificationTabPosition = 0
|
||||||
private var onTabSelectedListener: OnTabSelectedListener? = null
|
private var onTabSelectedListener: OnTabSelectedListener? = null
|
||||||
|
|
||||||
|
private var unreadAnnouncementsCount = 0
|
||||||
|
|
||||||
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
||||||
|
|
||||||
private val emojiInitCallback = object : InitCallback() {
|
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. */
|
* drawer, though, because its callback touches the header in the drawer. */
|
||||||
fetchUserInfo()
|
fetchUserInfo()
|
||||||
|
|
||||||
|
fetchAnnouncements()
|
||||||
|
|
||||||
setupTabs(showNotificationTab)
|
setupTabs(showNotificationTab)
|
||||||
|
|
||||||
// Setup push notifications
|
// Setup push notifications
|
||||||
|
@ -206,6 +214,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
when (event) {
|
when (event) {
|
||||||
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
|
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
|
||||||
is MainTabsChangedEvent -> setupTabs(false)
|
is MainTabsChangedEvent -> setupTabs(false)
|
||||||
|
is AnnouncementReadEvent -> {
|
||||||
|
unreadAnnouncementsCount--
|
||||||
|
updateAnnouncementsBadge()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,6 +404,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
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(),
|
DividerDrawerItem(),
|
||||||
secondaryDrawerItem {
|
secondaryDrawerItem {
|
||||||
nameRes = R.string.action_view_account_preferences
|
nameRes = R.string.action_view_account_preferences
|
||||||
|
@ -653,6 +677,25 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
updateShortcut(this, accountManager.activeAccount!!)
|
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() {
|
private fun updateProfiles() {
|
||||||
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
|
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
|
||||||
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header))
|
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 TAG = "MainActivity" // logging tag
|
||||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||||
private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10
|
private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10
|
||||||
|
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
||||||
const val STATUS_URL = "statusUrl"
|
const val STATUS_URL = "statusUrl"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,3 +20,4 @@ data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||||
|
data class AnnouncementReadEvent(val announcementId: String): Dispatchable
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
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<Announcement> = emptyList(),
|
||||||
|
private val listener: AnnouncementActionListener
|
||||||
|
) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() {
|
||||||
|
|
||||||
|
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<Announcement>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
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<Resource<List<Announcement>>>()
|
||||||
|
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
|
||||||
|
|
||||||
|
private val emojisMutable = MutableLiveData<List<Emoji>>()
|
||||||
|
val emojis: LiveData<List<Emoji>> = emojisMutable
|
||||||
|
|
||||||
|
init {
|
||||||
|
Singles.zip(
|
||||||
|
mastodonApi.getCustomEmojis(),
|
||||||
|
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||||
|
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||||
|
.onErrorResumeNext(
|
||||||
|
mastodonApi.getInstance()
|
||||||
|
.map { Either.Right<InstanceEntity, Instance>(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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,6 @@ import androidx.core.view.inputmethod.InputContentInfoCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
@ -301,7 +300,7 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
viewModel.media.observe { media ->
|
viewModel.media.observe { media ->
|
||||||
mediaAdapter.submitList(media)
|
mediaAdapter.submitList(media)
|
||||||
if(media.size != mediaCount) {
|
if (media.size != mediaCount) {
|
||||||
mediaCount = media.size
|
mediaCount = media.size
|
||||||
composeMediaPreviewBar.visible(media.isNotEmpty())
|
composeMediaPreviewBar.visible(media.isNotEmpty())
|
||||||
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
||||||
|
@ -311,8 +310,8 @@ class ComposeActivity : BaseActivity(),
|
||||||
pollPreview.visible(poll != null)
|
pollPreview.visible(poll != null)
|
||||||
poll?.let(pollPreview::setPoll)
|
poll?.let(pollPreview::setPoll)
|
||||||
}
|
}
|
||||||
viewModel.scheduledAt.observe {scheduledAt ->
|
viewModel.scheduledAt.observe { scheduledAt ->
|
||||||
if(scheduledAt == null) {
|
if (scheduledAt == null) {
|
||||||
composeScheduleView.resetSchedule()
|
composeScheduleView.resetSchedule()
|
||||||
} else {
|
} else {
|
||||||
composeScheduleView.setDateTime(scheduledAt)
|
composeScheduleView.setDateTime(scheduledAt)
|
||||||
|
@ -344,7 +343,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
scheduleBehavior = BottomSheetBehavior.from(composeScheduleView)
|
scheduleBehavior = BottomSheetBehavior.from(composeScheduleView)
|
||||||
emojiBehavior = BottomSheetBehavior.from(emojiView)
|
emojiBehavior = BottomSheetBehavior.from(emojiView)
|
||||||
|
|
||||||
emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false)
|
|
||||||
enableButton(composeEmojiButton, clickable = false, colorActive = false)
|
enableButton(composeEmojiButton, clickable = false, colorActive = false)
|
||||||
|
|
||||||
// Setup the interface buttons.
|
// Setup the interface buttons.
|
||||||
|
@ -552,7 +550,7 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onScheduleClick() {
|
private fun onScheduleClick() {
|
||||||
if(viewModel.scheduledAt.value == null) {
|
if (viewModel.scheduledAt.value == null) {
|
||||||
composeScheduleView.openPickDateDialog()
|
composeScheduleView.openPickDateDialog()
|
||||||
} else {
|
} else {
|
||||||
showScheduleView()
|
showScheduleView()
|
||||||
|
@ -715,9 +713,9 @@ class ComposeActivity : BaseActivity(),
|
||||||
// Verify the returned content's type is of the correct MIME type
|
// Verify the returned content's type is of the correct MIME type
|
||||||
val supported = inputContentInfo.description.hasMimeType("image/*")
|
val supported = inputContentInfo.description.hasMimeType("image/*")
|
||||||
|
|
||||||
if(supported) {
|
if (supported) {
|
||||||
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||||
if(lacksPermission) {
|
if (lacksPermission) {
|
||||||
try {
|
try {
|
||||||
inputContentInfo.requestPermission()
|
inputContentInfo.requestPermission()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -771,7 +769,7 @@ class ComposeActivity : BaseActivity(),
|
||||||
Snackbar.LENGTH_SHORT).apply {
|
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
|
//necessary so snackbar is shown over everything
|
||||||
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||||
bar.show()
|
bar.show()
|
||||||
|
@ -913,7 +911,7 @@ class ComposeActivity : BaseActivity(),
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
Log.d(TAG, event.toString())
|
Log.d(TAG, event.toString())
|
||||||
if(event.action == KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||||
if (event.isCtrlPressed) {
|
if (event.isCtrlPressed) {
|
||||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||||
// send toot by pressing CTRL + ENTER
|
// send toot by pressing CTRL + ENTER
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.*
|
||||||
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
|
@ -103,4 +104,7 @@ abstract class ActivitiesModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
|
@ -85,5 +86,10 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(ScheduledTootViewModel::class)
|
@ViewModelKey(ScheduledTootViewModel::class)
|
||||||
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
|
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(AnnouncementsViewModel::class)
|
||||||
|
internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
|
||||||
|
|
||||||
//Add more ViewModels here
|
//Add more ViewModels here
|
||||||
}
|
}
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
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<Status.Mention>,
|
||||||
|
val statuses: List<Status>,
|
||||||
|
val tags: List<HashTag>,
|
||||||
|
val emojis: List<Emoji>,
|
||||||
|
val reactions: List<Reaction>
|
||||||
|
) {
|
||||||
|
|
||||||
|
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?
|
||||||
|
)
|
||||||
|
}
|
|
@ -23,5 +23,6 @@ import kotlinx.android.parcel.Parcelize
|
||||||
data class Emoji(
|
data class Emoji(
|
||||||
val shortcode: String,
|
val shortcode: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
@SerializedName("static_url") val staticUrl: String,
|
||||||
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
|
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
|
||||||
) : Parcelable
|
) : Parcelable
|
|
@ -513,6 +513,28 @@ interface MastodonApi {
|
||||||
@Field("choices[]") choices: List<Int>
|
@Field("choices[]") choices: List<Int>
|
||||||
): Single<Poll>
|
): Single<Poll>
|
||||||
|
|
||||||
|
@GET("api/v1/announcements")
|
||||||
|
fun listAnnouncements(
|
||||||
|
@Query("with_dismissed") withDismissed: Boolean = true
|
||||||
|
): Single<List<Announcement>>
|
||||||
|
|
||||||
|
@POST("api/v1/announcements/{id}/dismiss")
|
||||||
|
fun dismissAnnouncement(
|
||||||
|
@Path("id") announcementId: String
|
||||||
|
): Single<ResponseBody>
|
||||||
|
|
||||||
|
@PUT("api/v1/announcements/{id}/reactions/{name}")
|
||||||
|
fun addAnnouncementReaction(
|
||||||
|
@Path("id") announcementId: String,
|
||||||
|
@Path("name") name: String
|
||||||
|
): Single<ResponseBody>
|
||||||
|
|
||||||
|
@DELETE("api/v1/announcements/{id}/reactions/{name}")
|
||||||
|
fun removeAnnouncementReaction(
|
||||||
|
@Path("id") announcementId: String,
|
||||||
|
@Path("name") name: String
|
||||||
|
): Single<ResponseBody>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/reports")
|
@POST("api/v1/reports")
|
||||||
fun reportObservable(
|
fun reportObservable(
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
9
app/src/main/res/drawable/ic_bullhorn_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_bullhorn_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,8H4A2,2 0,0 0,2 10V14A2,2 0,0 0,4 16H5V20A1,1 0,0 0,6 21H8A1,1 0,0 0,9 20V16H12L17,20V4L12,8M21.5,12C21.5,13.71 20.54,15.26 19,16V8C20.53,8.75 21.5,10.3 21.5,12Z" />
|
||||||
|
</vector>
|
39
app/src/main/res/layout/activity_announcements.xml
Normal file
39
app/src/main/res/layout/activity_announcements.xml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/announcementsList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||||
|
android:id="@+id/errorMessageView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@android:color/transparent"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:src="@drawable/elephant_error"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -193,14 +193,12 @@
|
||||||
android:textSize="?attr/status_text_medium" />
|
android:textSize="?attr/status_text_medium" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<com.keylesspalace.tusky.view.EmojiPicker
|
||||||
android:id="@+id/emojiView"
|
android:id="@+id/emojiView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:clipToPadding="false"
|
|
||||||
android:elevation="12dp"
|
android:elevation="12dp"
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
|
|
41
app/src/main/res/layout/item_announcement.xml
Normal file
41
app/src/main/res/layout/item_announcement.xml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lineSpacingMultiplier="1.1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.ChipGroup
|
||||||
|
android:id="@+id/chipGroup"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text">
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/addReactionChip"
|
||||||
|
style="@style/Widget.MaterialComponents.Chip.Action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkable="false"
|
||||||
|
app:chipEndPadding="4dp"
|
||||||
|
app:chipIcon="@drawable/ic_plus_24dp"
|
||||||
|
app:chipSurfaceColor="@color/tusky_blue"
|
||||||
|
app:textEndPadding="0dp"
|
||||||
|
app:textStartPadding="0dp" />
|
||||||
|
|
||||||
|
</com.google.android.material.chip.ChipGroup>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -42,6 +42,7 @@
|
||||||
<string name="title_edit_profile">Edit your profile</string>
|
<string name="title_edit_profile">Edit your profile</string>
|
||||||
<string name="title_saved_toot">Drafts</string>
|
<string name="title_saved_toot">Drafts</string>
|
||||||
<string name="title_scheduled_toot">Scheduled toots</string>
|
<string name="title_scheduled_toot">Scheduled toots</string>
|
||||||
|
<string name="title_announcements">Announcements</string>
|
||||||
<string name="title_licenses">Licenses</string>
|
<string name="title_licenses">Licenses</string>
|
||||||
|
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
|
@ -569,6 +570,7 @@
|
||||||
|
|
||||||
<string name="no_saved_status">You don\'t have any drafts.</string>
|
<string name="no_saved_status">You don\'t have any drafts.</string>
|
||||||
<string name="no_scheduled_status">You don\'t have any scheduled statuses.</string>
|
<string name="no_scheduled_status">You don\'t have any scheduled statuses.</string>
|
||||||
|
<string name="no_announcements">There are no announcements.</string>
|
||||||
<string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string>
|
<string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string>
|
||||||
<string name="pref_title_show_cards_in_timelines">Show link previews in timelines</string>
|
<string name="pref_title_show_cards_in_timelines">Show link previews in timelines</string>
|
||||||
<string name="pref_title_confirm_reblogs">Show confirmation dialog before boosting</string>
|
<string name="pref_title_confirm_reblogs">Show confirmation dialog before boosting</string>
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
|
|
||||||
<item name="swipeRefreshLayoutProgressSpinnerBackgroundColor">?attr/colorSurface</item>
|
<item name="swipeRefreshLayoutProgressSpinnerBackgroundColor">?attr/colorSurface</item>
|
||||||
|
|
||||||
|
<item name="chipStyle">@style/Widget.MaterialComponents.Chip.Choice</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="ViewMediaActivity.AppBarLayout" parent="ThemeOverlay.AppCompat">
|
<style name="ViewMediaActivity.AppBarLayout" parent="ThemeOverlay.AppCompat">
|
||||||
|
|
Loading…
Reference in a new issue