Merge pull request #3256 from nextcloud/issue-3221-notify-me-later

Notify Me Later About a Message
This commit is contained in:
Marcel Hibbe 2023-08-31 10:37:26 +02:00 committed by GitHub
commit 1672caff55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 896 additions and 6 deletions

View file

@ -40,6 +40,7 @@ import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
import com.nextcloud.talk.models.json.reactions.ReactionsOverall;
import com.nextcloud.talk.models.json.reminder.ReminderOverall;
import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
import com.nextcloud.talk.models.json.signaling.SignalingOverall;
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
@ -671,4 +672,18 @@ public interface NcApi {
@Query("text") String text,
@Query("toLanguage") String toLanguage,
@Nullable @Query("fromLanguage") String fromLanguage);
@GET
Observable<ReminderOverall> getReminder(@Header("Authorization") String authorization,
@Url String url);
@DELETE
Observable<GenericOverall> deleteReminder(@Header("Authorization") String authorization,
@Url String url);
@FormUrlEncoded
@POST
Observable<ReminderOverall> setReminder(@Header("Authorization") String authorization,
@Url String url,
@Field("timestamp") int timestamp);
}

View file

@ -95,6 +95,7 @@ import androidx.core.text.bold
import androidx.core.widget.doAfterTextChanged
import androidx.emoji2.text.EmojiCompat
import androidx.emoji2.widget.EmojiTextView
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -183,6 +184,7 @@ import com.nextcloud.talk.ui.MicInputCloud
import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.AttachmentDialog
import com.nextcloud.talk.ui.dialog.DateTimePickerFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
@ -3860,6 +3862,16 @@ class ChatActivity :
startActivity(intent)
}
fun remindMeLater(message: ChatMessage?) {
Log.d(TAG, "remindMeLater called")
val newFragment: DialogFragment = DateTimePickerFragment.newInstance(
roomToken,
message!!.id,
chatViewModel
)
newFragment.show(supportFragmentManager, DateTimePickerFragment.TAG)
}
fun markAsUnread(message: IMessage?) {
val chatMessage = message as ChatMessage?
if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {

View file

@ -22,10 +22,14 @@ package com.nextcloud.talk.chat.data
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder
import io.reactivex.Observable
interface ChatRepository {
fun getRoom(user: User, roomToken: String): Observable<ConversationModel>
fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable<ConversationModel>
fun setReminder(user: User, roomToken: String, messageId: String, timeStamp: Int): Observable<Reminder>
fun getReminder(user: User, roomToken: String, messageId: String): Observable<Reminder>
fun deleteReminder(user: User, roomToken: String, messageId: String): Observable<GenericOverall>
}

View file

@ -23,6 +23,8 @@ package com.nextcloud.talk.chat.data
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observable
@ -54,4 +56,38 @@ class ChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository {
roomPassword
).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) }
}
override fun setReminder(user: User, roomToken: String, messageId: String, timeStamp: Int): Observable<Reminder> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)
val apiVersion = ApiUtils.getChatApiVersion(user, intArrayOf(ApiUtils.APIv1, 1))
return ncApi.setReminder(
credentials,
ApiUtils.getUrlForReminder(user, roomToken, messageId, apiVersion),
timeStamp
).map {
it.ocs!!.data
}
}
override fun getReminder(user: User, roomToken: String, messageId: String): Observable<Reminder> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)
val apiVersion = ApiUtils.getChatApiVersion(user, intArrayOf(ApiUtils.APIv1, 1))
return ncApi.getReminder(
credentials,
ApiUtils.getUrlForReminder(user, roomToken, messageId, apiVersion)
).map {
it.ocs!!.data
}
}
override fun deleteReminder(user: User, roomToken: String, messageId: String): Observable<GenericOverall> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)
val apiVersion = ApiUtils.getChatApiVersion(user, intArrayOf(ApiUtils.APIv1, 1))
return ncApi.deleteReminder(
credentials,
ApiUtils.getUrlForReminder(user, roomToken, messageId, apiVersion)
).map {
it
}
}
}

View file

@ -27,6 +27,8 @@ import androidx.lifecycle.ViewModel
import com.nextcloud.talk.chat.data.ChatRepository
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@ -40,6 +42,13 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository)
object GetRoomStartState : ViewState
object GetRoomErrorState : ViewState
object GetReminderStartState : ViewState
open class GetReminderExistState(val reminder: Reminder) : ViewState
private val _getReminderExistState: MutableLiveData<ViewState> = MutableLiveData(GetReminderStartState)
val getReminderExistState: LiveData<ViewState>
get() = _getReminderExistState
open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState
private val _getRoomViewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomStartState)
@ -71,6 +80,43 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository)
?.subscribe(JoinRoomObserver())
}
fun setReminder(user: User, roomToken: String, messageId: String, timestamp: Int) {
repository.setReminder(user, roomToken, messageId, timestamp)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(SetReminderObserver())
}
fun getReminder(user: User, roomToken: String, messageId: String) {
repository.getReminder(user, roomToken, messageId)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(GetReminderObserver())
}
fun deleteReminder(user: User, roomToken: String, messageId: String) {
repository.deleteReminder(user, roomToken, messageId)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(genericOverall: GenericOverall) {
_getReminderExistState.value = GetReminderStartState
}
override fun onError(e: Throwable) {
Log.d(TAG, "Error when deleting reminder $e")
}
override fun onComplete() {
// unused atm
}
})
}
inner class GetRoomObserver : Observer<ConversationModel> {
override fun onSubscribe(d: Disposable) {
// unused atm
@ -109,6 +155,43 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository)
}
}
inner class SetReminderObserver : Observer<Reminder> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(reminder: Reminder) {
Log.d(TAG, "reminder set successfully")
}
override fun onError(e: Throwable) {
Log.e(TAG, "Error when sending reminder, $e")
}
override fun onComplete() {
// unused atm
}
}
inner class GetReminderObserver : Observer<Reminder> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(reminder: Reminder) {
_getReminderExistState.value = GetReminderExistState(reminder)
}
override fun onError(e: Throwable) {
Log.d(TAG, "Error when getting reminder $e")
_getReminderExistState.value = GetReminderStartState
}
override fun onComplete() {
// unused atm
}
}
companion object {
private val TAG = ChatViewModel::class.simpleName
const val JOIN_ROOM_RETRY_COUNT: Long = 3

View file

@ -175,7 +175,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
} else if (isSpreedNotification()) {
Log.d(TAG, "pushMessage.type: " + pushMessage.type)
when (pushMessage.type) {
TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING -> handleNonCallPushMessage()
TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER -> handleNonCallPushMessage()
TYPE_CALL -> handleCallPushMessage()
else -> Log.e(TAG, "unknown pushMessage.type")
}
@ -407,7 +407,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
) {
var category = ""
when (pushMessage.type) {
TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING -> category = Notification.CATEGORY_MESSAGE
TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER -> category = Notification.CATEGORY_MESSAGE
TYPE_CALL -> category = Notification.CATEGORY_CALL
else -> Log.e(TAG, "unknown pushMessage.type")
}
@ -464,7 +464,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (pushMessage.type) {
TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING -> {
TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER -> {
notificationBuilder.setChannelId(
NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name
)
@ -489,7 +489,9 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
val systemNotificationId: Int =
activeStatusBarNotification?.id ?: calculateCRC32(System.currentTimeMillis().toString()).toInt()
if (TYPE_CHAT == pushMessage.type && pushMessage.notificationUser != null) {
if ((TYPE_CHAT == pushMessage.type || TYPE_REMINDER == pushMessage.type) &&
pushMessage.notificationUser != null
) {
prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId)
addReplyAction(notificationBuilder, systemNotificationId)
addMarkAsReadAction(notificationBuilder, systemNotificationId)
@ -522,6 +524,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
else -> // assuming one2one
largeIcon = if (TYPE_CHAT == pushMessage.type || TYPE_ROOM == pushMessage.type) {
ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!!
} else if (TYPE_REMINDER == pushMessage.type) {
ContextCompat.getDrawable(context!!, R.drawable.ic_timer_black_24dp)?.toBitmap()!!
} else {
ContextCompat.getDrawable(context!!, R.drawable.ic_call_black_24dp)?.toBitmap()!!
}
@ -984,6 +988,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
private const val TYPE_ROOM = "room"
private const val TYPE_CALL = "call"
private const val TYPE_RECORDING = "recording"
private const val TYPE_REMINDER = "reminder"
private const val SPREED_APP = "spreed"
private const val TIMER_START = 1
private const val TIMER_COUNT = 12

View file

@ -0,0 +1,41 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
*
* 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.reminder
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class Reminder(
@JsonField(name = ["userid"])
var userid: String? = null,
@JsonField(name = ["token"])
var token: String? = null,
@JsonField(name = ["messageId"])
var messageId: Int? = null,
@JsonField(name = ["timestamp"])
var timestamp: Int? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null, null, null)
}

View file

@ -0,0 +1,38 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
*
* 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.reminder
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.generic.GenericMeta
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class ReminderOCS(
@JsonField(name = ["meta"])
var meta: GenericMeta? = null,
@JsonField(name = ["data"])
var data: Reminder? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null)
}

View file

@ -0,0 +1,35 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
*
* 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.reminder
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class ReminderOverall(
@JsonField(name = ["ocs"])
var ocs: ReminderOCS? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null)
}

View file

@ -0,0 +1,309 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linu@nextcloud.com>
*
* 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.ui.dialog
import android.app.Dialog
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import autodagger.AutoInjector
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.timepicker.MaterialTimePicker
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.databinding.DialogDateTimePickerBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import java.util.Calendar
import java.util.TimeZone
import javax.inject.Inject
@Suppress("TooManyFunctions")
@AutoInjector(NextcloudTalkApplication::class)
class DateTimePickerFragment(
token: String,
id: String,
chatViewModel: ChatViewModel
) : DialogFragment() {
lateinit var binding: DialogDateTimePickerBinding
private var dialogView: View? = null
private var viewModel = chatViewModel
private var currentTimeStamp: Long? = null
private var roomToken = token
private var messageId = id
private var laterTodayTimeStamp = 0L
private var tomorrowTimeStamp = 0L
private var weekendTimeStamp = 0L
private var nextWeekTimeStamp = 0L
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogDateTimePickerBinding.inflate(LayoutInflater.from(context))
dialogView = binding.root
return MaterialAlertDialogBuilder(requireContext()).setView(dialogView).create()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
setUpDefaults()
setUpColors()
setListeners()
getReminder()
viewModel.getReminderExistState.observe(this) { state ->
when (state) {
is ChatViewModel.GetReminderExistState -> {
val timeStamp = state.reminder.timestamp?.toLong()?.times(ONE_SEC)
showDelete(true)
setTimeStamp(getTimeFromTimeStamp(timeStamp!!))
}
else -> {
showDelete(false)
binding.dateTimePickerTimestamp.text = ""
}
}
}
return inflater.inflate(R.layout.dialog_date_time_picker, container, false)
}
private fun setUpDefaults() {
val currTime = getTimeFromCalendar()
val currentWeekInYear = Calendar.getInstance().get(Calendar.WEEK_OF_YEAR)
laterTodayTimeStamp = getTimeFromCalendar(hour = HOUR_SIX_PM, minute = 0)
binding.dateTimePickerLaterTodayTextview.text = getTimeFromTimeStamp(laterTodayTimeStamp)
if (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
tomorrowTimeStamp = getTimeFromCalendar(
hour = HOUR_EIGHT_AM,
minute = 0,
daysToAdd = 1,
weekInYear =
currentWeekInYear + 1
)
binding.dateTimePickerWeekend.visibility = View.GONE // because today is the weekend
} else {
tomorrowTimeStamp = getTimeFromCalendar(hour = HOUR_EIGHT_AM, minute = 0, daysToAdd = 1)
weekendTimeStamp = getTimeFromCalendar(hour = HOUR_EIGHT_AM, day = Calendar.SATURDAY, minute = 0)
}
binding.dateTimePickerTomorrowTextview.text = getTimeFromTimeStamp(tomorrowTimeStamp)
binding.dateTimePickerWeekendTextview.text = getTimeFromTimeStamp(weekendTimeStamp)
nextWeekTimeStamp = getTimeFromCalendar(
hour = HOUR_EIGHT_AM,
day = Calendar.MONDAY,
minute = 0,
weekInYear =
currentWeekInYear + 1
) // this should only pick mondays from next week only
binding.dateTimePickerNextWeekTextview.text = getTimeFromTimeStamp(nextWeekTimeStamp)
// This is to hide the later today option, if it's past 6pm
if (currTime > laterTodayTimeStamp) {
binding.dateTimePickerLaterToday.visibility = View.GONE
}
// This is to hide the tomorrow option, if that's also the weekend
if (binding.dateTimePickerTomorrowTextview.text == binding.dateTimePickerWeekendTextview.text) {
binding.dateTimePickerTomorrow.visibility = View.GONE
}
}
private fun getReminder() {
viewModel.getReminder(userManager.currentUser.blockingGet(), roomToken, messageId)
}
private fun showDelete(value: Boolean) {
if (value) {
binding.buttonDelete.visibility = View.VISIBLE
} else {
binding.buttonDelete.visibility = View.GONE
}
}
private fun setUpColors() {
binding.root.let {
viewThemeUtils.platform.colorViewBackground(it)
}
binding.dateTimePickerCustomIcon.let {
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
}
binding.dateTimePickerTimestamp.let {
viewThemeUtils.material.themeSearchBarText(it)
}
binding.run {
listOf(
binding.buttonClose,
binding.buttonSet
)
}.forEach(viewThemeUtils.material::colorMaterialButtonPrimaryBorderless)
}
private fun setListeners() {
binding.dateTimePickerLaterToday.setOnClickListener {
currentTimeStamp = laterTodayTimeStamp / ONE_SEC
setTimeStamp(getTimeFromTimeStamp(laterTodayTimeStamp))
}
binding.dateTimePickerTomorrow.setOnClickListener {
currentTimeStamp = tomorrowTimeStamp / ONE_SEC
setTimeStamp(getTimeFromTimeStamp(tomorrowTimeStamp))
}
binding.dateTimePickerWeekend.setOnClickListener {
currentTimeStamp = weekendTimeStamp / ONE_SEC
setTimeStamp(getTimeFromTimeStamp(weekendTimeStamp))
}
binding.dateTimePickerNextWeek.setOnClickListener {
currentTimeStamp = nextWeekTimeStamp / ONE_SEC
setTimeStamp(getTimeFromTimeStamp(nextWeekTimeStamp))
}
binding.dateTimePickerCustom.setOnClickListener {
val constraintsBuilder = CalendarConstraints.Builder()
.setValidator(DateValidatorPointForward.now())
.build()
val time = System.currentTimeMillis()
val datePicker = MaterialDatePicker.Builder.datePicker()
.setTitleText(R.string.nc_remind)
.setSelection(time + TimeZone.getDefault().getOffset(time))
.setCalendarConstraints(constraintsBuilder).build()
datePicker.addOnPositiveButtonClickListener { selection ->
val localTimeInMillis = selection - TimeZone.getDefault().getOffset(selection)
val calendar = Calendar.getInstance()
calendar.timeInMillis = localTimeInMillis
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH)
val day = calendar.get(Calendar.DAY_OF_WEEK)
val weekInYear = calendar.get(Calendar.WEEK_OF_YEAR)
setUpTimePicker(year, month, day, weekInYear)
}
datePicker.show(this.parentFragmentManager, TAG)
}
binding.buttonClose.setOnClickListener { dismiss() }
binding.buttonSet.setOnClickListener {
currentTimeStamp?.let { time ->
viewModel.setReminder(userManager.currentUser.blockingGet(), roomToken, messageId, time.toInt())
}
dismiss()
}
binding.buttonDelete.setOnClickListener {
viewModel.deleteReminder(userManager.currentUser.blockingGet(), roomToken, messageId)
}
}
private fun setUpTimePicker(year: Int, month: Int, day: Int, weekInYear: Int) {
val timePicker = MaterialTimePicker
.Builder()
.setTitleText(R.string.nc_remind)
.build()
timePicker.addOnPositiveButtonClickListener {
val timestamp = getTimeFromCalendar(
year,
month,
day,
timePicker.hour,
timePicker.minute,
weekInYear = weekInYear
)
setTimeStamp(getTimeFromTimeStamp(timestamp))
currentTimeStamp = timestamp / ONE_SEC
}
timePicker.show(this.parentFragmentManager, TAG)
}
@Suppress("LongParameterList")
private fun getTimeFromCalendar(
year: Int = Calendar.getInstance().get(Calendar.YEAR),
month: Int = Calendar.getInstance().get(Calendar.MONTH),
day: Int = Calendar.getInstance().get(Calendar.DAY_OF_WEEK),
hour: Int = Calendar.getInstance().get(Calendar.HOUR_OF_DAY),
minute: Int = Calendar.getInstance().get(Calendar.MINUTE),
daysToAdd: Int = 0,
weekInYear: Int = Calendar.getInstance().get(Calendar.WEEK_OF_YEAR)
): Long {
val calendar: Calendar = Calendar.getInstance().apply {
set(Calendar.YEAR, year)
set(Calendar.MONTH, month)
set(Calendar.DAY_OF_WEEK, day)
add(Calendar.DAY_OF_WEEK, daysToAdd)
set(Calendar.WEEK_OF_YEAR, weekInYear)
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
}
return calendar.timeInMillis
}
private fun setTimeStamp(date: String) {
binding.dateTimePickerTimestamp.text = date
}
private fun getTimeFromTimeStamp(time: Long): String {
return DateUtils.formatDateTime(
requireContext(),
time,
DateUtils.FORMAT_SHOW_DATE
) + ", " + DateUtils.formatDateTime(
requireContext(),
time,
DateUtils.FORMAT_SHOW_TIME
)
}
companion object {
val TAG = DateTimePickerFragment::class.simpleName
private const val ONE_SEC = 1000
private const val HOUR_EIGHT_AM = 8
private const val HOUR_SIX_PM = 18
@JvmStatic
fun newInstance(
token: String,
id: String,
chatViewModel: ChatViewModel
) = DateTimePickerFragment(
token,
id,
chatViewModel
)
}
}

View file

@ -109,6 +109,7 @@ class MessageActionsDialog(
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
!(message.isDeletedCommentMessage || message.isDeleted)
)
initMenuRemindMessage(!message.isDeleted && CapabilitiesUtilNew.isRemindSupported(user))
initMenuMarkAsUnread(
message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType()
@ -264,6 +265,17 @@ class MessageActionsDialog(
dialogMessageActionsBinding.menuForwardMessage.visibility = getVisibility(visible)
}
private fun initMenuRemindMessage(visible: Boolean) {
if (visible) {
dialogMessageActionsBinding.menuNotifyMessage.setOnClickListener {
chatActivity.remindMeLater(message)
dismiss()
}
}
dialogMessageActionsBinding.menuNotifyMessage.visibility = getVisibility(visible)
}
private fun initMenuDeleteMessage(visible: Boolean) {
if (visible) {
dialogMessageActionsBinding.menuDeleteMessage.setOnClickListener {

View file

@ -530,4 +530,9 @@ public class ApiUtils {
public static String getUrlForTranslation(String baseUrl) {
return baseUrl + ocsApiVersion + "/translation/translate";
}
public static String getUrlForReminder(User user, String roomToken, String messageId, int version) {
String url = ApiUtils.getUrlForChatMessage(version, user.getBaseUrl(), roomToken, messageId);
return url + "/reminder";
}
}

View file

@ -145,7 +145,7 @@ object CapabilitiesUtilNew {
}
@JvmStatic
fun getAttachmentFolder(user: User): String? {
fun getAttachmentFolder(user: User): String {
if (user.capabilities?.spreedCapability?.config?.containsKey("attachments") == true) {
val map = user.capabilities!!.spreedCapability!!.config!!["attachments"]
if (map?.containsKey("folder") == true) {
@ -241,5 +241,14 @@ object CapabilitiesUtilNew {
}
}
fun isRemindSupported(user: User?): Boolean {
if (user?.capabilities != null) {
val capabilities = user.capabilities
return capabilities?.spreedCapability?.features?.contains("remind-me-later") == true
}
return false
}
const val DEFAULT_CHAT_SIZE = 1000
}

View file

@ -0,0 +1,27 @@
<!--
@author Google LLC
Copyright (C) 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z" />
</vector>

View file

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Julius Linus
~ Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
~
~ 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.
~
~ This program 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 <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/nc_remind"
android:layout_weight="1"
android:textSize="@dimen/md_title_textsize" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date_time_picker_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Apr 15th, 8:00 AM"
android:textSize="@dimen/supporting_text_text_size"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/date_time_picker_later_today"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/later_today"
android:textSize="@dimen/headline_text_size" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date_time_picker_later_today_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/headline_text_size"
android:text="" />
</LinearLayout>
<LinearLayout
android:id="@+id/date_time_picker_tomorrow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/tomorrow"
android:textSize="@dimen/headline_text_size" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date_time_picker_tomorrow_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/headline_text_size"
android:text="" />
</LinearLayout>
<LinearLayout
android:id="@+id/date_time_picker_weekend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/this_weekend"
android:textSize="@dimen/headline_text_size" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date_time_picker_weekend_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/headline_text_size"
android:text="" />
</LinearLayout>
<LinearLayout
android:id="@+id/date_time_picker_next_week"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/next_week"
android:textSize="@dimen/headline_text_size" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date_time_picker_next_week_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/headline_text_size"
android:text="" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/date_time_picker_custom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/date_time_picker_custom_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/baseline_calendar_month_24"
android:paddingEnd="@dimen/standard_double_padding"
tools:ignore="RtlSymmetry"
android:contentDescription="@string/calendar" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/custom"
android:layout_weight="1"
android:textSize="@dimen/headline_text_size" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="1">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_delete"
style="@style/Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_size_clickable_area"
android:text="@string/nc_delete"
android:textColor="@color/design_default_color_error" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_set"
style="@style/Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_size_clickable_area"
android:text="@string/set" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_close"
style="@style/Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:minHeight="@dimen/min_size_clickable_area"
android:text="@string/close" />
</LinearLayout>
</LinearLayout>

View file

@ -221,6 +221,39 @@
</LinearLayout>
<LinearLayout
android:id="@+id/menu_notify_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_notify_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:src="@drawable/ic_timer_black_24dp"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_notify_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/standard_padding"
android:text="@string/nc_remind"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/menu_mark_as_unread"
android:layout_width="match_parent"

View file

@ -697,5 +697,13 @@ How to translate with transifex:
<string name="nc_settings_socks_value" translatable="false">1080</string>
<string name="this_is_a_test_message">This is a test message</string>
<string name="continuous_voice_message_recording">Lock recording for continuously recording of the voice message</string>
<string name="nc_remind">Remind me later</string>
<string name="next_week">Next week</string>
<string name="this_weekend">This weekend</string>
<string name="tomorrow">Tomorrow</string>
<string name="later_today">Later today</string>
<string name="custom">Custom</string>
<string name="set">Set</string>
<string name="calendar">Calendar</string>
</resources>