diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt new file mode 100644 index 00000000..61618d4e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt @@ -0,0 +1,247 @@ +package com.keylesspalace.tusky.adapter + +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.text.TextUtils +import android.text.format.DateUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.ChatMessage +import com.keylesspalace.tusky.interfaces.ChatActionListener +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.view.MediaPreviewImageView +import com.keylesspalace.tusky.viewdata.ChatMessageViewData +import com.keylesspalace.tusky.viewdata.ChatViewData +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.roundToInt + +class ChatMessagesViewHolder(view: View) : RecyclerView.ViewHolder(view) { + object Key { + const val KEY_CREATED = "created" + } + + private val content: TextView = view.findViewById(R.id.content) + private val timestamp: TextView = view.findViewById(R.id.datetime) + private val attachmentView: MediaPreviewImageView = view.findViewById(R.id.attachment) + private val mediaOverlay: ImageView = view.findViewById(R.id.mediaOverlay) + private val attachmentLayout: FrameLayout = view.findViewById(R.id.attachmentLayout) + + private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()) + + private val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(itemView.context, R.attr.colorBackgroundAccent)) + + fun setupWithChatMessage(msg: ChatMessageViewData.Concrete, chatActionListener: ChatActionListener, statusDisplayOptions: StatusDisplayOptions, payload: Any?) { + if(payload == null) { + content.text = msg.content.emojify(msg.emojis, content) + setAttachment(msg.attachment, chatActionListener) + setCreatedAt(msg.createdAt, statusDisplayOptions) + } else { + if(payload is List<*>) { + for (item in payload) { + if (ChatsViewHolder.Key.KEY_CREATED == item) { + setCreatedAt(msg.createdAt, statusDisplayOptions) + } + } + } + } + } + + private fun loadImage(imageView: MediaPreviewImageView, + previewUrl: String?, + meta: Attachment.MetaData?) { + if (TextUtils.isEmpty(previewUrl)) { + imageView.removeFocalPoint() + Glide.with(imageView) + .load(mediaPreviewUnloaded) + .centerInside() + .into(imageView) + } else { + val focus = meta?.focus + if (focus != null) { // If there is a focal point for this attachment: + imageView.setFocalPoint(focus) + Glide.with(imageView) + .load(previewUrl) + .placeholder(mediaPreviewUnloaded) + .centerInside() + .addListener(imageView) + .into(imageView) + } else { + imageView.removeFocalPoint() + Glide.with(imageView) + .load(previewUrl) + .placeholder(mediaPreviewUnloaded) + .centerInside() + .into(imageView) + } + } + } + + private fun formatDuration(durationInSeconds: Double): String? { + val seconds = durationInSeconds.roundToInt().toInt() % 60 + val minutes = durationInSeconds.toInt() % 3600 / 60 + val hours = durationInSeconds.toInt() / 3600 + return String.format("%d:%02d:%02d", hours, minutes, seconds) + } + + private fun getAttachmentDescription(context: Context, attachment: Attachment): CharSequence { + var duration = "" + if (attachment.meta?.duration != null && attachment.meta.duration > 0) { + duration = formatDuration(attachment.meta.duration.toDouble()) + " " + } + return if (TextUtils.isEmpty(attachment.description)) { + duration + context.getString(R.string.description_status_media_no_description_placeholder) + } else { + duration + attachment.description + } + } + + + private fun setAttachmentClickListener(view: View, listener: ChatActionListener, attachment: Attachment, animateTransition: Boolean) { + view.setOnClickListener { v: View? -> + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onViewMedia(position, if (animateTransition) v else null) + } + } + view.setOnLongClickListener { v: View? -> + val description = getAttachmentDescription(view.context, attachment) + Toast.makeText(view.context, description, Toast.LENGTH_LONG).show() + true + } + } + + + private fun setAttachment(attachment: Attachment?, listener: ChatActionListener) { + if(attachment == null) { + attachmentLayout.visibility = View.GONE + } else { + attachmentLayout.visibility = View.VISIBLE + + val previewUrl: String = attachment.previewUrl + val description: String? = attachment.description + + if(description != null && TextUtils.isEmpty(description) ) { + attachmentView.contentDescription = description + } else { + attachmentView.contentDescription = attachmentView.context + .getString(R.string.action_view_media) + } + + loadImage(attachmentView, previewUrl, attachment.meta) + + when(attachment.type) { + Attachment.Type.VIDEO, Attachment.Type.GIFV -> { + mediaOverlay.visibility = View.VISIBLE + } + else -> { + mediaOverlay.visibility = View.GONE + } + } + + setAttachmentClickListener(attachmentView, listener, attachment, true) + } + } + + private fun getAbsoluteTime(createdAt: Date?): String? { + if (createdAt == null) { + return "??:??:??" + } + return if (DateUtils.isToday(createdAt.time)) { + shortSdf.format(createdAt) + } else { + longSdf.format(createdAt) + } + } + + private fun setCreatedAt(createdAt: Date, statusDisplayOptions: StatusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime) { + timestamp.text = getAbsoluteTime(createdAt) + } else { + val then = createdAt.time + val now = System.currentTimeMillis() + val readout = TimestampUtils.getRelativeTimeSpanString(timestamp.context, then, now) + timestamp.text = readout + } + } +} + +class ChatMessagesAdapter(private val dataSource : TimelineAdapter.AdapterDataSource, + private val chatActionListener: ChatActionListener, + private val statusDisplayOptions: StatusDisplayOptions, + private val localUserId: String) +: RecyclerView.Adapter() { + + private val VIEW_TYPE_OUR_MESSAGE = 0 + private val VIEW_TYPE_THEIR_MESSAGE = 1 + private val VIEW_TYPE_PLACEHOLDER = 2 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + when(viewType) { + VIEW_TYPE_OUR_MESSAGE -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_our_message, parent, false) + return ChatMessagesViewHolder(view) + } + VIEW_TYPE_THEIR_MESSAGE -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_their_message, parent, false) + return ChatMessagesViewHolder(view) + } + else -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status_placeholder, parent, false) + return PlaceholderViewHolder(view) + } + } + } + + override fun getItemCount(): Int { + return dataSource.itemCount + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + bindViewHolder(holder, position, null) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList) { + bindViewHolder(holder, position, payload) + } + + private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList?) { + val chat: ChatMessageViewData = dataSource.getItemAt(position) + if(holder is PlaceholderViewHolder) { + holder.setup(chatActionListener, (chat as ChatMessageViewData.Placeholder).isLoading) + } else if(holder is ChatMessagesViewHolder) { + holder.setupWithChatMessage(chat as ChatMessageViewData.Concrete, chatActionListener, statusDisplayOptions, + if (payloads != null && payloads.isNotEmpty()) payloads[0] else null) + } + } + + override fun getItemViewType(position: Int): Int { + if(dataSource.getItemAt(position) is ChatMessageViewData.Concrete) { + val msg = dataSource.getItemAt(position) as ChatMessageViewData.Concrete + + if(msg.accountId == localUserId) { + return VIEW_TYPE_OUR_MESSAGE + } + return VIEW_TYPE_THEIR_MESSAGE + } + return VIEW_TYPE_PLACEHOLDER + } + + override fun getItemId(position: Int): Long { + return dataSource.getItemAt(position).getViewDataId() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt index c6320562..f143a13e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt @@ -2,44 +2,124 @@ package com.keylesspalace.tusky.components.chat import android.content.Context import android.content.Intent -import android.graphics.drawable.Drawable import android.os.Bundle +import android.util.Log import android.view.MenuItem import android.view.View -import com.bumptech.glide.Glide -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition -import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.adapter.ChatMessagesAdapter +import com.keylesspalace.tusky.adapter.ChatMessagesViewHolder +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.adapter.TimelineAdapter import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Chat +import com.keylesspalace.tusky.entity.ChatMessage import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.interfaces.ChatActionListener import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.repository.ChatMessageStatus import com.keylesspalace.tusky.repository.ChatRepository -import com.keylesspalace.tusky.util.StatusDisplayOptions -import com.keylesspalace.tusky.util.emojify -import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.repository.ChatStatus +import com.keylesspalace.tusky.viewdata.ChatMessageViewData +import com.keylesspalace.tusky.viewdata.ChatViewData import kotlinx.android.synthetic.main.activity_chat.* -import kotlinx.android.synthetic.main.toolbar_basic.* import kotlinx.android.synthetic.main.toolbar_basic.toolbar +import androidx.arch.core.util.Function +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.* +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.repository.TimelineRequestMode +import com.keylesspalace.tusky.util.* +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.activity_chat.progressBar +import kotlinx.android.synthetic.main.fragment_timeline.* +import java.lang.Exception import javax.inject.Inject -class ChatActivity: BaseActivity(), - Injectable { +class ChatActivity: BottomSheetActivity(), + Injectable, ChatActionListener { + private val TAG = "ChatsF" // logging tag + private val LOAD_AT_ONCE = 30 @Inject lateinit var eventHub: EventHub - @Inject lateinit var api: MastodonApi - @Inject lateinit var chatsRepo: ChatRepository + lateinit var adapter: ChatMessagesAdapter + + private val msgs = PairedList(Function {input -> + input.asRightOrNull()?.let(ViewDataUtils::chatMessageToViewData) ?: + ChatMessageViewData.Placeholder(input.asLeft().id, false) + }) + + private val listUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + Log.d(TAG, "onInserted") + adapter.notifyItemRangeInserted(position, count) + if (position == 0) { + recycler.scrollToPosition(0) + } + } + + override fun onRemoved(position: Int, count: Int) { + Log.d(TAG, "onRemoved") + adapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + Log.d(TAG, "onMoved") + adapter.notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + Log.d(TAG, "onChanged") + adapter.notifyItemRangeChanged(position, count, payload) + } + } + + private val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Boolean { + return oldItem.getViewDataId() == newItem.getViewDataId() + } + + override fun areContentsTheSame(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Any? { + return if (oldItem.deepEquals(newItem)) { + //If items are equal - update timestamp only + listOf(ChatMessagesViewHolder.Key.KEY_CREATED) + } else // If items are different - update a whole view holder + null + } + } + + private val differ = AsyncListDiffer(listUpdateCallback, + AsyncDifferConfig.Builder(diffCallback).build()) + + private val dataSource = object : TimelineAdapter.AdapterDataSource { + override fun getItemCount(): Int { + return differ.currentList.size + } + + override fun getItemAt(pos: Int): ChatMessageViewData { + return differ.currentList[pos] + } + } + + private lateinit var chatId : String + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val chatId = intent.getStringExtra(ID) + chatId = intent.getStringExtra(ID) val avatarUrl = intent.getStringExtra(AVATAR_URL) val displayName = intent.getStringExtra(DISPLAY_NAME) val username = intent.getStringExtra(USERNAME) @@ -49,6 +129,10 @@ class ChatActivity: BaseActivity(), throw IllegalArgumentException("Can't open ChatActivity without chat id") } + if(accountManager.activeAccount == null) { + throw Exception("No active account!") + } + setContentView(R.layout.activity_chat) setSupportActionBar(toolbar) @@ -63,6 +147,50 @@ class ChatActivity: BaseActivity(), chatTitle.text = displayName.emojify(emojis, chatTitle, true) chatUsername.text = username + + val statusDisplayOptions = StatusDisplayOptions( + false, + false, + true, + false, + false, + CardViewMode.NONE, + false) + + adapter = ChatMessagesAdapter(dataSource, this, statusDisplayOptions, accountManager.activeAccount!!.accountId) + + // TODO: a11y + recycler.setHasFixedSize(true) + val layoutManager = LinearLayoutManager(this) + layoutManager.reverseLayout = true + recycler.layoutManager = layoutManager + recycler.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + recycler.adapter = adapter + + tryCache() + } + + private fun tryCache() { + // Request timeline from disk to make it quick, then replace it with timeline from + // the server to update it + chatsRepo.getChatMessages(chatId, null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { msgs -> + if (msgs.size > 1) { + val mutableChats = msgs.toMutableList() + this.msgs.clear() + this.msgs.addAll(mutableChats) + updateAdapter() + progressBar.visibility = View.GONE + // Request statuses including current top to refresh all of them + } + } + } + + private fun updateAdapter() { + Log.d(TAG, "updateAdapter") + differ.submitList(msgs.pairedCopy) } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -75,6 +203,20 @@ class ChatActivity: BaseActivity(), return super.onOptionsItemSelected(item) } + override fun onViewAccount(id: String) { + viewAccount(id) + } + + override fun onViewUrl(url: String) { + viewUrl(url) + } + + override fun onViewTag(tag: String) { + val intent = Intent(this, ViewTagActivity::class.java) + intent.putExtra("hashtag", tag) + startActivity(intent) + } + companion object { fun getIntent(context: Context, chat: Chat) : Intent { val intent = Intent(context, ChatActivity::class.java) @@ -92,5 +234,4 @@ class ChatActivity: BaseActivity(), const val USERNAME = "username" const val EMOJIS = "emojis" } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt index f4c590e0..5b02da91 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt @@ -115,7 +115,7 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta } } - private val diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + private val diffCallback = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ChatViewData, newItem: ChatViewData): Boolean { return oldItem.getViewDataId() == newItem.getViewDataId() } @@ -136,7 +136,7 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta private val differ = AsyncListDiffer(listUpdateCallback, AsyncDifferConfig.Builder(diffCallback).build()) - private val dataSource: TimelineAdapter.AdapterDataSource = object : TimelineAdapter.AdapterDataSource { + private val dataSource = object : TimelineAdapter.AdapterDataSource { override fun getItemCount(): Int { return differ.currentList.size } diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt index c6955612..0a25b58d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt @@ -4,9 +4,8 @@ import android.view.View import com.keylesspalace.tusky.entity.Chat interface ChatActionListener: LinkListener { - fun onLoadMore(position: Int) - - fun onMore(chatId: String, v: View) - - fun openChat(position: Int) + fun onLoadMore(position: Int) {} + fun onMore(chatId: String, v: View) {} + fun openChat(position: Int) {} + fun onViewMedia(position: Int, view: View?) {} } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt index 0843dadf..b3a59416 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt @@ -23,10 +23,13 @@ import java.util.concurrent.TimeUnit import kotlin.collections.ArrayList typealias ChatStatus = Either +typealias ChatMessageStatus = Either interface ChatRepository { fun getChats(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode): Single> + + fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single> } class ChatRepositoryImpl( @@ -49,6 +52,13 @@ class ChatRepositoryImpl( } } + override fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single> { + val acc = accountManager.activeAccount ?: throw IllegalStateException() + val accountId = acc.id + + return getChatMessagesFromNetwork(chatId, maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) + } + private fun getChatsFromNetwork(maxId: String?, sinceId: String?, sinceIdMinusOne: String?, limit: Int, accountId: Long, requestMode: TimelineRequestMode @@ -69,6 +79,16 @@ class ChatRepositoryImpl( } } + private fun getChatMessagesFromNetwork(chatId: String, maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, limit: Int, + accountId: Long, requestMode: TimelineRequestMode + ): Single> { + return mastodonApi.getChatMessages(chatId, maxId, null, sinceIdMinusOne, 0, limit + 1).map { + it.mapTo(mutableListOf(), ChatMessage::lift) + } + } + + private fun addFromDbIfNeeded(accountId: Long, chats: List, maxId: String?, sinceId: String?, limit: Int, requestMode: TimelineRequestMode @@ -234,4 +254,6 @@ fun ChatEntityWithAccount.toChat(gson: Gson) : ChatStatus { ).lift() } -fun Chat.lift(): Either = Either.Right(this) +fun ChatMessage.lift(): ChatMessageStatus = Either.Right(this) + +fun Chat.lift(): ChatStatus = Either.Right(this) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt index 9bc9cbfa..2b998ade 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt @@ -77,7 +77,7 @@ abstract class ChatMessageViewData { } } - class Placeholder(val id: String, private val isLoading: Boolean) : ChatMessageViewData() { + class Placeholder(val id: String, val isLoading: Boolean) : ChatMessageViewData() { override fun getViewDataId(): Long { return id.hashCode().toLong() } diff --git a/app/src/main/res/drawable/message_background.xml b/app/src/main/res/drawable/message_background.xml new file mode 100644 index 00000000..36c66061 --- /dev/null +++ b/app/src/main/res/drawable/message_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 90d59f46..a49204ef 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -190,4 +190,13 @@ app:layout_constraintRight_toRightOf="parent" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_our_message.xml b/app/src/main/res/layout/item_our_message.xml new file mode 100644 index 00000000..3960ebd8 --- /dev/null +++ b/app/src/main/res/layout/item_our_message.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_their_message.xml b/app/src/main/res/layout/item_their_message.xml new file mode 100644 index 00000000..5e6b828a --- /dev/null +++ b/app/src/main/res/layout/item_their_message.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 3dd3e30f..885f1791 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -19,4 +19,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index d4a84d46..66a640c8 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -51,4 +51,15 @@ 108dp 16dp + + 16dp + 64dp + 4dp + 8dp + 10dp + 10dp + 36dp + 8dp + 12dp + 160dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ddc192c7..7770d303 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -79,6 +79,11 @@ ?attr/colorSurface + @color/tusky_blue + @color/textColorPrimary + @color/colorPrimaryDark + @color/textColorPrimary + @color/textColorSecondary + + + +