add content to NotificationsFragment
This commit is contained in:
parent
1c7a82b472
commit
c1a9897c2a
|
@ -78,9 +78,8 @@ dependencies {
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||||
implementation("androidx.preference:preference:1.1.1")
|
implementation("androidx.preference:preference:1.1.1")
|
||||||
implementation("androidx.emoji:emoji-bundled:1.1.0")
|
implementation("androidx.emoji:emoji-bundled:1.1.0")
|
||||||
implementation("androidx.paging:paging-runtime-ktx:3.0.0-alpha05")
|
implementation("androidx.paging:paging-runtime-ktx:3.0.0-alpha06")
|
||||||
implementation("androidx.viewpager2:viewpager2:1.0.0")
|
implementation("androidx.viewpager2:viewpager2:1.0.0")
|
||||||
implementation("androidx.window:window:1.0.0-alpha01")
|
|
||||||
|
|
||||||
implementation("androidx.room:room-ktx:$roomVersion")
|
implementation("androidx.room:room-ktx:$roomVersion")
|
||||||
kapt("androidx.room:room-compiler:$roomVersion")
|
kapt("androidx.room:room-compiler:$roomVersion")
|
||||||
|
|
|
@ -25,7 +25,6 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Conny Duck
|
||||||
|
*
|
||||||
|
* This file is part of Pixelcat.
|
||||||
|
*
|
||||||
|
* Pixelcat 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.
|
||||||
|
*
|
||||||
|
* Pixelcat 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package at.connyduck.pixelcat.components.notifications
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.text.parseAsHtml
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import at.connyduck.pixelcat.R
|
||||||
|
import at.connyduck.pixelcat.components.util.BindingHolder
|
||||||
|
import at.connyduck.pixelcat.components.util.extension.hide
|
||||||
|
import at.connyduck.pixelcat.databinding.ItemNotificationBinding
|
||||||
|
import at.connyduck.pixelcat.databinding.ItemNotificationFollowBinding
|
||||||
|
import at.connyduck.pixelcat.databinding.ItemReplyBinding
|
||||||
|
import at.connyduck.pixelcat.db.entitity.NotificationEntity
|
||||||
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
|
import at.connyduck.pixelcat.db.entitity.TimelineAccountEntity
|
||||||
|
import at.connyduck.pixelcat.model.Notification
|
||||||
|
import coil.load
|
||||||
|
import coil.transform.RoundedCornersTransformation
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
interface NotificationActionListener {
|
||||||
|
fun onDetailsOpened(status: StatusEntity)
|
||||||
|
fun onProfileOpened(account: TimelineAccountEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
object NotificationDiffUtil : DiffUtil.ItemCallback<NotificationEntity>() {
|
||||||
|
override fun areItemsTheSame(oldItem: NotificationEntity, newItem: NotificationEntity): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: NotificationEntity, newItem: NotificationEntity): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationAdapter(
|
||||||
|
private val listener: NotificationActionListener
|
||||||
|
) : PagingDataAdapter<NotificationEntity, BindingHolder<*>>(NotificationDiffUtil) {
|
||||||
|
|
||||||
|
private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<*> {
|
||||||
|
val binding = when (viewType) {
|
||||||
|
MENTION -> ItemReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
FOLLOW -> ItemNotificationFollowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
FAVOURITE -> ItemNotificationBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
REBLOG -> ItemNotificationBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
return BindingHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingHolder<*>, position: Int) {
|
||||||
|
getItem(position)?.let { notification ->
|
||||||
|
when (holder.binding) {
|
||||||
|
is ItemReplyBinding -> holder.binding.bind(notification, dateTimeFormatter)
|
||||||
|
is ItemNotificationFollowBinding -> holder.binding.bind(notification, listener)
|
||||||
|
is ItemNotificationBinding -> holder.binding.bind(notification, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
Log.d("NotificationAdapter", "$position ${getItem(position)}")
|
||||||
|
return when (getItem(position)?.type) {
|
||||||
|
Notification.Type.MENTION -> MENTION
|
||||||
|
Notification.Type.REBLOG -> REBLOG
|
||||||
|
Notification.Type.FAVOURITE -> FAVOURITE
|
||||||
|
Notification.Type.FOLLOW -> FOLLOW
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MENTION = 1
|
||||||
|
private const val FOLLOW = 2
|
||||||
|
private const val FAVOURITE = 3
|
||||||
|
private const val REBLOG = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ItemReplyBinding.bind(notification: NotificationEntity, dateTimeFormatter: DateFormat) {
|
||||||
|
val status = notification.status!!
|
||||||
|
|
||||||
|
postAvatar.load(status.account.avatar) {
|
||||||
|
transformations(RoundedCornersTransformation(25f))
|
||||||
|
}
|
||||||
|
|
||||||
|
postDisplayName.text = status.account.displayName
|
||||||
|
postName.text = "@${status.account.username}"
|
||||||
|
|
||||||
|
postDescription.text = status.content.parseAsHtml().trim()
|
||||||
|
|
||||||
|
postDate.text = dateTimeFormatter.format(status.createdAt)
|
||||||
|
|
||||||
|
postLikeButton.hide()
|
||||||
|
|
||||||
|
postReplyButton.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ItemNotificationFollowBinding.bind(notification: NotificationEntity, listener: NotificationActionListener) {
|
||||||
|
|
||||||
|
val account = notification.account
|
||||||
|
|
||||||
|
notificationText.text = notificationText.context.getString(R.string.notification_followed, notification.account.username)
|
||||||
|
|
||||||
|
notificationAvatar.load(account.avatar) {
|
||||||
|
transformations(RoundedCornersTransformation(25f))
|
||||||
|
}
|
||||||
|
notificationDisplayName.text = account.displayName
|
||||||
|
notificationName.text = account.username
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
listener.onProfileOpened(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ItemNotificationBinding.bind(notification: NotificationEntity, listener: NotificationActionListener) {
|
||||||
|
|
||||||
|
notificationAvatar.load(notification.account.avatar) {
|
||||||
|
transformations(RoundedCornersTransformation(25f))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.type == Notification.Type.REBLOG) {
|
||||||
|
notificationText.text = notificationText.context.getString(R.string.notification_reblogged, notification.account.username)
|
||||||
|
} else { // Notification.Type.FAVOURITE
|
||||||
|
notificationText.text = notificationText.context.getString(R.string.notification_favourited, notification.account.username)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
listener.onDetailsOpened(notification.status!!)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,18 +19,77 @@
|
||||||
|
|
||||||
package at.connyduck.pixelcat.components.notifications
|
package at.connyduck.pixelcat.components.notifications
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import at.connyduck.pixelcat.R
|
import at.connyduck.pixelcat.R
|
||||||
|
import at.connyduck.pixelcat.components.profile.ProfileActivity
|
||||||
|
import at.connyduck.pixelcat.components.timeline.detail.DetailActivity
|
||||||
|
import at.connyduck.pixelcat.components.util.getColorForAttr
|
||||||
import at.connyduck.pixelcat.dagger.ViewModelFactory
|
import at.connyduck.pixelcat.dagger.ViewModelFactory
|
||||||
|
import at.connyduck.pixelcat.databinding.FragmentNotificationsBinding
|
||||||
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
|
import at.connyduck.pixelcat.db.entitity.TimelineAccountEntity
|
||||||
|
import at.connyduck.pixelcat.util.viewBinding
|
||||||
import dagger.android.support.DaggerFragment
|
import dagger.android.support.DaggerFragment
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class NotificationsFragment : DaggerFragment(R.layout.fragment_notifications) {
|
class NotificationsFragment :
|
||||||
|
DaggerFragment(R.layout.fragment_notifications),
|
||||||
|
NotificationActionListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
private val notificationsViewModel: NotificationsViewModel by viewModels { viewModelFactory }
|
private val viewModel: NotificationsViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
private val binding by viewBinding(FragmentNotificationsBinding::bind)
|
||||||
|
|
||||||
|
@ExperimentalPagingApi
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
binding.notificationSwipeRefresh.setColorSchemeColors(
|
||||||
|
view.context.getColorForAttr(R.attr.pixelcat_gradient_color_start),
|
||||||
|
view.context.getColorForAttr(R.attr.pixelcat_gradient_color_end)
|
||||||
|
)
|
||||||
|
|
||||||
|
val adapter = NotificationAdapter(this)
|
||||||
|
|
||||||
|
binding.notificationRecyclerView.adapter = adapter
|
||||||
|
(binding.notificationRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
binding.notificationRecyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.notificationsFlow.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.notificationSwipeRefresh.setOnRefreshListener {
|
||||||
|
adapter.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.addLoadStateListener {
|
||||||
|
if (it.refresh != LoadState.Loading) {
|
||||||
|
binding.notificationSwipeRefresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetailsOpened(status: StatusEntity) {
|
||||||
|
startActivity(DetailActivity.newIntent(requireContext(), status.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProfileOpened(account: TimelineAccountEntity) {
|
||||||
|
startActivity(ProfileActivity.newIntent(requireContext(), account.id))
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = NotificationsFragment()
|
fun newInstance() = NotificationsFragment()
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Conny Duck
|
||||||
|
*
|
||||||
|
* This file is part of Pixelcat.
|
||||||
|
*
|
||||||
|
* Pixelcat 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.
|
||||||
|
*
|
||||||
|
* Pixelcat 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package at.connyduck.pixelcat.components.notifications
|
||||||
|
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadType
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.RemoteMediator
|
||||||
|
import androidx.room.withTransaction
|
||||||
|
import at.connyduck.pixelcat.db.AppDatabase
|
||||||
|
import at.connyduck.pixelcat.db.entitity.NotificationEntity
|
||||||
|
import at.connyduck.pixelcat.db.entitity.toEntity
|
||||||
|
import at.connyduck.pixelcat.network.FediverseApi
|
||||||
|
|
||||||
|
@ExperimentalPagingApi
|
||||||
|
class NotificationsRemoteMediator(
|
||||||
|
private val accountId: Long,
|
||||||
|
private val api: FediverseApi,
|
||||||
|
private val db: AppDatabase
|
||||||
|
) : RemoteMediator<Int, NotificationEntity>() {
|
||||||
|
|
||||||
|
override suspend fun load(
|
||||||
|
loadType: LoadType,
|
||||||
|
state: PagingState<Int, NotificationEntity>
|
||||||
|
): MediatorResult {
|
||||||
|
val apiCall = when (loadType) {
|
||||||
|
LoadType.REFRESH -> {
|
||||||
|
api.notifications(limit = state.config.initialLoadSize, excludes = setOf("poll", "follow_request"))
|
||||||
|
}
|
||||||
|
LoadType.PREPEND -> {
|
||||||
|
return MediatorResult.Success(true)
|
||||||
|
}
|
||||||
|
LoadType.APPEND -> {
|
||||||
|
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.id
|
||||||
|
api.notifications(maxId = maxId, limit = state.config.pageSize, excludes = setOf("poll", "follow_request"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiCall.fold(
|
||||||
|
{ notificationResult ->
|
||||||
|
db.withTransaction {
|
||||||
|
if (loadType == LoadType.REFRESH) {
|
||||||
|
db.notificationsDao().clearAll(accountId)
|
||||||
|
}
|
||||||
|
db.notificationsDao().insertOrReplace(notificationResult.map { it.toEntity(accountId) })
|
||||||
|
}
|
||||||
|
MediatorResult.Success(endOfPaginationReached = notificationResult.isEmpty())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MediatorResult.Error(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun initialize() = InitializeAction.SKIP_INITIAL_REFRESH
|
||||||
|
}
|
|
@ -20,6 +20,35 @@
|
||||||
package at.connyduck.pixelcat.components.notifications
|
package at.connyduck.pixelcat.components.notifications
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import at.connyduck.pixelcat.db.AccountManager
|
||||||
|
import at.connyduck.pixelcat.db.AppDatabase
|
||||||
|
import at.connyduck.pixelcat.network.FediverseApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class NotificationsViewModel @Inject constructor() : ViewModel()
|
class NotificationsViewModel @Inject constructor(
|
||||||
|
accountManager: AccountManager,
|
||||||
|
private val db: AppDatabase,
|
||||||
|
private val fediverseApi: FediverseApi
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
@ExperimentalPagingApi
|
||||||
|
val notificationsFlow = accountManager::activeAccount.asFlow()
|
||||||
|
.flatMapConcat { activeAccount ->
|
||||||
|
Pager(
|
||||||
|
config = PagingConfig(pageSize = 10, enablePlaceholders = false),
|
||||||
|
remoteMediator = NotificationsRemoteMediator(activeAccount?.id!!, fediverseApi, db),
|
||||||
|
pagingSourceFactory = { db.notificationsDao().notifications(activeAccount.id) }
|
||||||
|
).flow
|
||||||
|
}
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ package at.connyduck.pixelcat.components.profile
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||||
|
|
|
@ -21,12 +21,14 @@ package at.connyduck.pixelcat.dagger
|
||||||
|
|
||||||
import at.connyduck.pixelcat.BuildConfig
|
import at.connyduck.pixelcat.BuildConfig
|
||||||
import at.connyduck.pixelcat.db.AccountManager
|
import at.connyduck.pixelcat.db.AccountManager
|
||||||
|
import at.connyduck.pixelcat.model.Notification
|
||||||
import at.connyduck.pixelcat.network.FediverseApi
|
import at.connyduck.pixelcat.network.FediverseApi
|
||||||
import at.connyduck.pixelcat.network.InstanceSwitchAuthInterceptor
|
import at.connyduck.pixelcat.network.InstanceSwitchAuthInterceptor
|
||||||
import at.connyduck.pixelcat.network.RefreshTokenAuthenticator
|
import at.connyduck.pixelcat.network.RefreshTokenAuthenticator
|
||||||
import at.connyduck.pixelcat.network.UserAgentInterceptor
|
import at.connyduck.pixelcat.network.UserAgentInterceptor
|
||||||
import at.connyduck.pixelcat.network.calladapter.NetworkResponseAdapterFactory
|
import at.connyduck.pixelcat.network.calladapter.NetworkResponseAdapterFactory
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapters.EnumJsonAdapter
|
||||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
@ -69,6 +71,7 @@ class NetworkModule {
|
||||||
fun providesMoshi(): Moshi {
|
fun providesMoshi(): Moshi {
|
||||||
return Moshi.Builder()
|
return Moshi.Builder()
|
||||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
|
.add(Notification.Type::class.java, EnumJsonAdapter.create(Notification.Type::class.java).withUnknownFallback(Notification.Type.UNKNOWN))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,15 @@ package at.connyduck.pixelcat.db
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import at.connyduck.pixelcat.db.entitity.AccountEntity
|
import at.connyduck.pixelcat.db.entitity.AccountEntity
|
||||||
|
import at.connyduck.pixelcat.db.entitity.NotificationEntity
|
||||||
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
|
|
||||||
@Database(entities = [AccountEntity::class, StatusEntity::class], version = 1)
|
@Database(entities = [AccountEntity::class, StatusEntity::class, NotificationEntity::class], version = 1)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun accountDao(): AccountDao
|
abstract fun accountDao(): AccountDao
|
||||||
|
|
||||||
abstract fun statusDao(): TimelineDao
|
abstract fun statusDao(): TimelineDao
|
||||||
|
|
||||||
|
abstract fun notificationsDao(): NotificationsDao
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ package at.connyduck.pixelcat.db
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import at.connyduck.pixelcat.model.Attachment
|
import at.connyduck.pixelcat.model.Attachment
|
||||||
|
import at.connyduck.pixelcat.model.Notification
|
||||||
import at.connyduck.pixelcat.model.Status
|
import at.connyduck.pixelcat.model.Status
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
|
@ -31,14 +32,10 @@ class Converters {
|
||||||
private val moshi = Moshi.Builder().build()
|
private val moshi = Moshi.Builder().build()
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun visibilityToInt(visibility: Status.Visibility): String {
|
fun visibilityToString(visibility: Status.Visibility) = visibility.name
|
||||||
return visibility.name
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToVisibility(visibility: String): Status.Visibility {
|
fun stringToVisibility(visibility: String) = Status.Visibility.valueOf(visibility)
|
||||||
return Status.Visibility.valueOf(visibility)
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
|
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
|
||||||
|
@ -62,12 +59,14 @@ class Converters {
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun dateToLong(date: Date): Long {
|
fun dateToLong(date: Date) = date.time
|
||||||
return date.time
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun longToDate(date: Long): Date {
|
fun longToDate(date: Long) = Date(date)
|
||||||
return Date(date)
|
|
||||||
}
|
@TypeConverter
|
||||||
|
fun notificationTypeToString(type: Notification.Type) = type.name
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun stringToNotificationType(type: String) = Notification.Type.valueOf(type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Conny Duck
|
||||||
|
*
|
||||||
|
* This file is part of Pixelcat.
|
||||||
|
*
|
||||||
|
* Pixelcat 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.
|
||||||
|
*
|
||||||
|
* Pixelcat 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package at.connyduck.pixelcat.db
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import at.connyduck.pixelcat.db.entitity.NotificationEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface NotificationsDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertOrReplace(notification: NotificationEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertOrReplace(statuses: List<NotificationEntity>)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(status: NotificationEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM NotificationEntity WHERE accountId = :accountId ORDER BY LENGTH(id) DESC, id DESC")
|
||||||
|
fun notifications(accountId: Long): PagingSource<Int, NotificationEntity>
|
||||||
|
|
||||||
|
@Query("DELETE FROM NotificationEntity WHERE accountId = :accountId")
|
||||||
|
suspend fun clearAll(accountId: Long)
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Conny Duck
|
||||||
|
*
|
||||||
|
* This file is part of Pixelcat.
|
||||||
|
*
|
||||||
|
* Pixelcat 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.
|
||||||
|
*
|
||||||
|
* Pixelcat 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package at.connyduck.pixelcat.db.entitity
|
||||||
|
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import at.connyduck.pixelcat.db.Converters
|
||||||
|
import at.connyduck.pixelcat.model.Notification
|
||||||
|
|
||||||
|
@Entity(primaryKeys = ["accountId", "id"])
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
data class NotificationEntity(
|
||||||
|
val accountId: Long,
|
||||||
|
val type: Notification.Type,
|
||||||
|
val id: String,
|
||||||
|
@Embedded(prefix = "a_") val account: TimelineAccountEntity,
|
||||||
|
@Embedded(prefix = "s_") val status: StatusEntity?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Notification.toEntity(accountId: Long) = NotificationEntity(
|
||||||
|
accountId = accountId,
|
||||||
|
type = type,
|
||||||
|
id = id,
|
||||||
|
account = account.toEntity(accountId),
|
||||||
|
status = status?.toEntity(accountId, 0, false)
|
||||||
|
)
|
|
@ -19,14 +19,10 @@
|
||||||
|
|
||||||
package at.connyduck.pixelcat.db.entitity
|
package at.connyduck.pixelcat.db.entitity
|
||||||
|
|
||||||
import androidx.room.Entity
|
|
||||||
import at.connyduck.pixelcat.model.Account
|
import at.connyduck.pixelcat.model.Account
|
||||||
|
|
||||||
@Entity(
|
|
||||||
primaryKeys = ["serverId", "timelineUserId"]
|
|
||||||
)
|
|
||||||
data class TimelineAccountEntity(
|
data class TimelineAccountEntity(
|
||||||
val serverId: Long,
|
val accountId: Long,
|
||||||
val id: String,
|
val id: String,
|
||||||
val localUsername: String,
|
val localUsername: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
|
@ -35,8 +31,8 @@ data class TimelineAccountEntity(
|
||||||
val avatar: String
|
val avatar: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Account.toEntity(serverId: Long) = TimelineAccountEntity(
|
fun Account.toEntity(accountId: Long) = TimelineAccountEntity(
|
||||||
serverId = serverId,
|
accountId = accountId,
|
||||||
id = id,
|
id = id,
|
||||||
localUsername = localUsername,
|
localUsername = localUsername,
|
||||||
username = username,
|
username = username,
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Conny Duck
|
||||||
|
*
|
||||||
|
* This file is part of Pixelcat.
|
||||||
|
*
|
||||||
|
* Pixelcat 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.
|
||||||
|
*
|
||||||
|
* Pixelcat 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package at.connyduck.pixelcat.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Notification(
|
||||||
|
val type: Type,
|
||||||
|
val id: String,
|
||||||
|
val account: Account,
|
||||||
|
val status: Status?
|
||||||
|
) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = false)
|
||||||
|
enum class Type {
|
||||||
|
UNKNOWN,
|
||||||
|
@Json(name = "mention")
|
||||||
|
MENTION,
|
||||||
|
@Json(name = "reblog")
|
||||||
|
REBLOG,
|
||||||
|
@Json(name = "favourite")
|
||||||
|
FAVOURITE,
|
||||||
|
@Json(name = "follow")
|
||||||
|
FOLLOW,
|
||||||
|
@Json(name = "follow_request")
|
||||||
|
FOLLOW_REQUEST,
|
||||||
|
@Json(name = "poll")
|
||||||
|
POLL
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import at.connyduck.pixelcat.model.Account
|
||||||
import at.connyduck.pixelcat.model.AppCredentials
|
import at.connyduck.pixelcat.model.AppCredentials
|
||||||
import at.connyduck.pixelcat.model.Attachment
|
import at.connyduck.pixelcat.model.Attachment
|
||||||
import at.connyduck.pixelcat.model.NewStatus
|
import at.connyduck.pixelcat.model.NewStatus
|
||||||
|
import at.connyduck.pixelcat.model.Notification
|
||||||
import at.connyduck.pixelcat.model.Relationship
|
import at.connyduck.pixelcat.model.Relationship
|
||||||
import at.connyduck.pixelcat.model.Status
|
import at.connyduck.pixelcat.model.Status
|
||||||
import at.connyduck.pixelcat.model.StatusContext
|
import at.connyduck.pixelcat.model.StatusContext
|
||||||
|
@ -40,6 +41,7 @@ import retrofit2.http.Part
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
@JvmSuppressWildcards
|
||||||
interface FediverseApi {
|
interface FediverseApi {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -199,4 +201,12 @@ interface FediverseApi {
|
||||||
suspend fun statusContext(
|
suspend fun statusContext(
|
||||||
@Path("id") statusId: String
|
@Path("id") statusId: String
|
||||||
): NetworkResponse<StatusContext>
|
): NetworkResponse<StatusContext>
|
||||||
|
|
||||||
|
@GET("api/v1/notifications")
|
||||||
|
suspend fun notifications(
|
||||||
|
@Query("max_id") maxId: String? = null,
|
||||||
|
@Query("since_id") sinceId: String? = null,
|
||||||
|
@Query("limit") limit: Int? = null,
|
||||||
|
@Query("exclude_types[]") excludes: Set<String>? = null
|
||||||
|
): NetworkResponse<List<Notification>>
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,23 +15,24 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:navigationIcon="@drawable/ic_cat_small"
|
app:navigationIcon="@drawable/ic_cat_small"
|
||||||
app:title="@string/app_name"
|
app:title="@string/title_notifications"
|
||||||
app:titleTextAppearance="@style/TextAppearanceToolbar"
|
app:titleTextAppearance="@style/TextAppearanceToolbar"
|
||||||
app:titleTextColor="?attr/colorPrimary" />
|
app:titleTextColor="?attr/colorPrimary" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/timelineSwipeRefresh"
|
android:id="@+id/notificationSwipeRefresh"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/timelineRecyclerView"
|
android:id="@+id/notificationRecyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/notificationAvatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:background="#f00" />
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/notificationText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/notificationAvatar"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/notificationAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/notificationAvatar"
|
||||||
|
tools:text="Conny Duck liked your post!" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/notificationText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="\@ConnyDuck followed you" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/notificationAvatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/notificationText"
|
||||||
|
tools:background="#f00" />
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/notificationDisplayName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/notificationName"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/notificationAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/notificationAvatar"
|
||||||
|
tools:text="Conny Duck" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notificationName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/notificationAvatar"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/notificationAvatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/notificationDisplayName"
|
||||||
|
tools:text="\@connyduck\@chaos.social" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -9,12 +9,12 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_search"
|
android:id="@+id/navigation_search"
|
||||||
android:icon="@drawable/ic_search"
|
android:icon="@drawable/ic_search"
|
||||||
android:title="@string/title_dashboard"/>
|
android:title="@string/title_search"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_compose"
|
android:id="@+id/navigation_compose"
|
||||||
android:icon="@drawable/ic_plus_square"
|
android:icon="@drawable/ic_plus_square"
|
||||||
android:title="@string/title_notifications"/>
|
android:title="@string/title_compose"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_notifications"
|
android:id="@+id/navigation_notifications"
|
||||||
android:icon="@drawable/ic_heart"
|
android:icon="@drawable/ic_heart"
|
||||||
|
@ -22,6 +22,6 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_profile"
|
android:id="@+id/navigation_profile"
|
||||||
android:icon="@drawable/ic_user"
|
android:icon="@drawable/ic_user"
|
||||||
android:title="@string/title_notifications"/>
|
android:title="@string/title_profile"/>
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Pixelcat</string>
|
<string name="app_name">Pixelcat</string>
|
||||||
<string name="title_home">Home</string>
|
<string name="title_home">Home</string>
|
||||||
<string name="title_dashboard">Dashboard</string>
|
<string name="title_search">Search</string>
|
||||||
|
<string name="title_compose">Compose new status</string>
|
||||||
<string name="title_notifications">Notifications</string>
|
<string name="title_notifications">Notifications</string>
|
||||||
|
<string name="title_profile">Profile</string>
|
||||||
|
|
||||||
<string name="login_button">Login</string>
|
<string name="login_button">Login</string>
|
||||||
<string name="instance_input_hint">Which instance?</string>
|
<string name="instance_input_hint">Which instance?</string>
|
||||||
|
@ -79,7 +81,10 @@
|
||||||
<string name="status_general_error">An unexpected error occurred</string>
|
<string name="status_general_error">An unexpected error occurred</string>
|
||||||
<string name="status_network_error">Failed to connect. Please check your internet connection-</string>
|
<string name="status_network_error">Failed to connect. Please check your internet connection-</string>
|
||||||
|
|
||||||
<string name="status_details_replying_to">Replying to $1%s</string>
|
<string name="status_details_replying_to">Replying to %1$s</string>
|
||||||
|
|
||||||
|
<string name="notification_reblogged">%1$s boosted your post</string>
|
||||||
|
<string name="notification_favourited">%1$s liked your post</string>
|
||||||
|
<string name="notification_followed">%1$s followed you</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue