Эх сурвалжийг харах

Notify me Later

- Added new Dialog Fragment
- Added API functions for handling reminders
- Added JSON Models for those reminders
- Implemented the reminder functions using MVVM

Signed-off-by: Julius Linus <julius.linus@nextcloud.com>
Julius Linus 1 жил өмнө
parent
commit
96437a133e

+ 15 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -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);
 }

+ 12 - 0
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

@@ -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) {

+ 5 - 1
app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt

@@ -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>
 }

+ 36 - 0
app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt

@@ -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
+        }
+    }
 }

+ 83 - 0
app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

@@ -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

+ 9 - 4
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt

@@ -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

+ 41 - 0
app/src/main/java/com/nextcloud/talk/models/json/reminder/Reminder.kt

@@ -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)
+}

+ 38 - 0
app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOCS.kt

@@ -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)
+}

+ 35 - 0
app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOverall.kt

@@ -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)
+}

+ 309 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimePickerFragment.kt

@@ -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
+        )
+    }
+}

+ 12 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt

@@ -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 {

+ 5 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -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";
+    }
 }

+ 10 - 1
app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt

@@ -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
 }

+ 27 - 0
app/src/main/res/drawable/baseline_calendar_month_24.xml

@@ -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>

+ 218 - 0
app/src/main/res/layout/dialog_date_time_picker.xml

@@ -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>

+ 33 - 0
app/src/main/res/layout/dialog_message_actions.xml

@@ -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"

+ 8 - 0
app/src/main/res/values/strings.xml

@@ -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>