[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 automaticallymain
parent
666d2be102
commit
aa14e6e4f2
@ -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" |
||||
} |
||||
} |
@ -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? |
||||
) |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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> |
@ -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> |
@ -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> |
Loading…
Reference in new issue