Skip to content

Commit ccd89d3

Browse files
committed
feat: status choosing
Signed-off-by: alperozturk <[email protected]>
1 parent e7e2422 commit ccd89d3

28 files changed

+1282
-0
lines changed

app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ dependencies {
152152
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
153153
implementation 'androidx.work:work-runtime:2.10.5'
154154
implementation 'com.google.android.material:material:1.13.0'
155+
156+
// Vanitech
157+
implementation 'com.vanniktech:emoji-google:0.21.0'
155158

156159
// Database
157160
implementation "androidx.room:room-runtime:${roomVersion}"

app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import android.graphics.drawable.LayerDrawable;
1515
import android.net.Uri;
1616
import android.os.Bundle;
17+
import android.view.View;
1718

1819
import androidx.annotation.NonNull;
1920
import androidx.fragment.app.DialogFragment;
@@ -75,6 +76,20 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
7576
binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost());
7677
AvatarLoader.INSTANCE.load(requireContext(), binding.currentAccountItemAvatar, currentLocalAccount);
7778
binding.accountLayout.setOnClickListener((v) -> dismiss());
79+
binding.onlineStatus.setOnClickListener(v -> {
80+
/*
81+
val setStatusDialog = SetOnlineStatusBottomSheet(currentStatus)
82+
setStatusDialog.show((activity as DrawerActivity).supportFragmentManager, "fragment_set_status")
83+
*/
84+
85+
dismiss();
86+
});
87+
88+
binding.statusMessage.setOnClickListener(v -> {
89+
final var setStatusMessageDialog = new SetStatusMessageBottomSheet(accountManager.user, currentStatus);
90+
setStatusMessageDialog.show(requireActivity().getSupportFragmentManager(), "fragment_set_status_message");
91+
dismiss();
92+
});
7893

7994
final var adapter = new AccountSwitcherAdapter((localAccount -> {
8095
accountSwitcherListener.onAccountChosen(localAccount);
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
* @author Tobias Kaminsky
5+
* Copyright (C) 2020 Nextcloud GmbH
6+
*
7+
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
8+
*/
9+
10+
package it.niedermann.owncloud.notes.accountswitcher
11+
12+
import android.annotation.SuppressLint
13+
import android.content.Context
14+
import android.os.Bundle
15+
import android.os.Handler
16+
import android.os.Looper
17+
import android.view.LayoutInflater
18+
import android.view.View
19+
import android.view.ViewGroup
20+
import android.view.inputmethod.InputMethodManager
21+
import android.widget.AdapterView
22+
import android.widget.AdapterView.OnItemSelectedListener
23+
import android.widget.ArrayAdapter
24+
import androidx.annotation.VisibleForTesting
25+
import androidx.recyclerview.widget.LinearLayoutManager
26+
import com.nextcloud.common.User
27+
import com.owncloud.android.lib.resources.users.ClearAt
28+
import com.owncloud.android.lib.resources.users.PredefinedStatus
29+
import com.owncloud.android.lib.resources.users.Status
30+
import com.vanniktech.emoji.EmojiManager
31+
import com.vanniktech.emoji.EmojiPopup
32+
import com.vanniktech.emoji.google.GoogleEmojiProvider
33+
import com.vanniktech.emoji.installDisableKeyboardInput
34+
import com.vanniktech.emoji.installForceSingleEmoji
35+
import it.niedermann.owncloud.notes.R
36+
import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusClickListener
37+
import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusListAdapter
38+
import it.niedermann.owncloud.notes.accountswitcher.task.ClearStatusTask
39+
import it.niedermann.owncloud.notes.accountswitcher.task.SetPredefinedCustomStatusTask
40+
import it.niedermann.owncloud.notes.accountswitcher.task.SetUserDefinedCustomStatusTask
41+
import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment
42+
import it.niedermann.owncloud.notes.branding.BrandingUtil
43+
import it.niedermann.owncloud.notes.databinding.SetStatusMessageBottomSheetBinding
44+
import it.niedermann.owncloud.notes.shared.util.DisplayUtils
45+
import it.niedermann.owncloud.notes.util.runner.AsyncRunner
46+
import it.niedermann.owncloud.notes.util.runner.ThreadPoolAsyncRunner
47+
import it.niedermann.owncloud.notes.util.storage.UserStorage
48+
import java.util.Calendar
49+
import java.util.Locale
50+
51+
private const val POS_DONT_CLEAR = 0
52+
private const val POS_HALF_AN_HOUR = 1
53+
private const val POS_AN_HOUR = 2
54+
private const val POS_FOUR_HOURS = 3
55+
private const val POS_TODAY = 4
56+
private const val POS_END_OF_WEEK = 5
57+
58+
private const val ONE_SECOND_IN_MILLIS = 1000
59+
private const val ONE_MINUTE_IN_SECONDS = 60
60+
private const val THIRTY_MINUTES = 30
61+
private const val FOUR_HOURS = 4
62+
private const val LAST_HOUR_OF_DAY = 23
63+
private const val LAST_MINUTE_OF_HOUR = 59
64+
private const val LAST_SECOND_OF_MINUTE = 59
65+
66+
private const val CLEAR_AT_TYPE_PERIOD = "period"
67+
private const val CLEAR_AT_TYPE_END_OF = "end-of"
68+
69+
class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) :
70+
BrandedBottomSheetDialogFragment(R.layout.set_status_message_bottom_sheet),
71+
PredefinedStatusClickListener,
72+
Injectable {
73+
74+
private lateinit var binding: SetStatusMessageBottomSheetBinding
75+
76+
private lateinit var accountManager: UserAccountManager
77+
private lateinit var predefinedStatus: ArrayList<PredefinedStatus>
78+
private lateinit var adapter: PredefinedStatusListAdapter
79+
private var selectedPredefinedMessageId: String? = null
80+
private var clearAt: Long? = -1
81+
private lateinit var popup: EmojiPopup
82+
83+
lateinit var asyncRunner: AsyncRunner
84+
85+
override fun onCreate(savedInstanceState: Bundle?) {
86+
super.onCreate(savedInstanceState)
87+
88+
val uiHandler = Handler(Looper.getMainLooper())
89+
asyncRunner = ThreadPoolAsyncRunner(uiHandler, 4, "io")
90+
predefinedStatus = UserStorage.readPredefinedStatus(requireContext())
91+
EmojiManager.install(GoogleEmojiProvider())
92+
}
93+
94+
@SuppressLint("DefaultLocale")
95+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
96+
super.onViewCreated(view, savedInstanceState)
97+
accountManager = (activity as BaseActivity).userAccountManager
98+
99+
currentStatus?.let {
100+
updateCurrentStatusViews(it)
101+
}
102+
103+
adapter = PredefinedStatusListAdapter(this, requireContext())
104+
if (this::predefinedStatus.isInitialized) {
105+
adapter.list = predefinedStatus
106+
}
107+
binding.predefinedStatusList.adapter = adapter
108+
binding.predefinedStatusList.layoutManager = LinearLayoutManager(context)
109+
110+
binding.clearStatus.setOnClickListener { clearStatus() }
111+
binding.setStatus.setOnClickListener { setStatusMessage() }
112+
binding.emoji.setOnClickListener { popup.show() }
113+
114+
popup = EmojiPopup(view, binding.emoji, onEmojiClickListener = { _ ->
115+
popup.dismiss()
116+
binding.emoji.clearFocus()
117+
val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as
118+
InputMethodManager
119+
imm.hideSoftInputFromWindow(binding.emoji.windowToken, 0)
120+
})
121+
binding.emoji.installForceSingleEmoji()
122+
binding.emoji.installDisableKeyboardInput(popup)
123+
124+
val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item)
125+
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
126+
adapter.add(getString(R.string.dontClear))
127+
adapter.add(getString(R.string.thirtyMinutes))
128+
adapter.add(getString(R.string.oneHour))
129+
adapter.add(getString(R.string.fourHours))
130+
adapter.add(getString(R.string.today))
131+
adapter.add(getString(R.string.thisWeek))
132+
133+
binding.clearStatusAfterSpinner.apply {
134+
this.adapter = adapter
135+
onItemClickListener = object : OnItemSelectedListener, AdapterView.OnItemClickListener {
136+
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
137+
setClearStatusAfterValue(position)
138+
}
139+
140+
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
141+
142+
override fun onItemClick(
143+
parent: AdapterView<*>?,
144+
view: View?,
145+
position: Int,
146+
id: Long
147+
) = Unit
148+
}
149+
}
150+
}
151+
152+
override fun applyBrand(color: Int) {
153+
val viewThemeUtils = BrandingUtil.of(color, requireContext())
154+
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.clearStatus)
155+
viewThemeUtils.material.colorMaterialButtonPrimaryTonal(binding.setStatus)
156+
viewThemeUtils.material.colorTextInputLayout(binding.customStatusInputContainer)
157+
viewThemeUtils.platform.themeDialog(binding.root)
158+
}
159+
160+
private fun updateCurrentStatusViews(it: Status) {
161+
if (it.icon.isNullOrBlank()) {
162+
binding.emoji.setText("😀")
163+
} else {
164+
binding.emoji.setText(it.icon)
165+
}
166+
167+
binding.customStatusInput.text?.clear()
168+
binding.customStatusInput.setText(it.message)
169+
170+
if (it.clearAt > 0) {
171+
binding.clearStatusAfterSpinner.visibility = View.GONE
172+
binding.remainingClearTime.apply {
173+
binding.clearStatusMessageTextView.text = getString(R.string.clear)
174+
visibility = View.VISIBLE
175+
text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true)
176+
.toString()
177+
.replaceFirstChar { it.lowercase(Locale.getDefault()) }
178+
setOnClickListener {
179+
visibility = View.GONE
180+
binding.clearStatusAfterSpinner.visibility = View.VISIBLE
181+
binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after)
182+
}
183+
}
184+
}
185+
}
186+
187+
private fun setClearStatusAfterValue(item: Int) {
188+
clearAt = when (item) {
189+
POS_DONT_CLEAR -> null // don't clear
190+
POS_HALF_AN_HOUR -> {
191+
// 30 minutes
192+
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS
193+
}
194+
195+
POS_AN_HOUR -> {
196+
// one hour
197+
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
198+
}
199+
200+
POS_FOUR_HOURS -> {
201+
// four hours
202+
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS +
203+
FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
204+
}
205+
206+
POS_TODAY -> {
207+
// today
208+
val date = getLastSecondOfToday()
209+
dateToSeconds(date)
210+
}
211+
212+
POS_END_OF_WEEK -> {
213+
// end of week
214+
val date = getLastSecondOfToday()
215+
while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
216+
date.add(Calendar.DAY_OF_YEAR, 1)
217+
}
218+
dateToSeconds(date)
219+
}
220+
221+
else -> clearAt
222+
}
223+
}
224+
225+
private fun clearAtToUnixTime(clearAt: ClearAt?): Long = when {
226+
clearAt?.type == CLEAR_AT_TYPE_PERIOD -> {
227+
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong()
228+
}
229+
230+
clearAt?.type == CLEAR_AT_TYPE_END_OF && clearAt.time == "day" -> {
231+
val date = getLastSecondOfToday()
232+
dateToSeconds(date)
233+
}
234+
235+
else -> -1
236+
}
237+
238+
private fun getLastSecondOfToday(): Calendar {
239+
val date = Calendar.getInstance().apply {
240+
set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
241+
set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
242+
set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
243+
}
244+
return date
245+
}
246+
247+
private fun dateToSeconds(date: Calendar) = date.timeInMillis / ONE_SECOND_IN_MILLIS
248+
249+
private fun clearStatus() {
250+
asyncRunner.postQuickTask(
251+
ClearStatusTask(accountManager.currentOwnCloudAccount?.savedAccount, context),
252+
{ dismiss(it) }
253+
)
254+
}
255+
256+
private fun setStatusMessage() {
257+
if (selectedPredefinedMessageId != null) {
258+
asyncRunner.postQuickTask(
259+
SetPredefinedCustomStatusTask(
260+
selectedPredefinedMessageId!!,
261+
clearAt,
262+
accountManager.currentOwnCloudAccount?.savedAccount,
263+
context
264+
),
265+
{ dismiss(it) }
266+
)
267+
} else {
268+
asyncRunner.postQuickTask(
269+
SetUserDefinedCustomStatusTask(
270+
binding.customStatusInput.text.toString(),
271+
binding.emoji.text.toString(),
272+
clearAt,
273+
accountManager.currentOwnCloudAccount?.savedAccount,
274+
context
275+
),
276+
{ dismiss(it) }
277+
)
278+
}
279+
}
280+
281+
private fun dismiss(boolean: Boolean) {
282+
if (boolean) {
283+
dismiss()
284+
} else {
285+
DisplayUtils.showSnackMessage(view, view?.resources?.getString(R.string.error_setting_status_message))
286+
}
287+
}
288+
289+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
290+
binding = SetStatusMessageBottomSheetBinding.inflate(layoutInflater, container, false)
291+
return binding.root
292+
}
293+
294+
override fun onClick(predefinedStatus: PredefinedStatus) {
295+
selectedPredefinedMessageId = predefinedStatus.id
296+
clearAt = clearAtToUnixTime(predefinedStatus.clearAt)
297+
binding.emoji.setText(predefinedStatus.icon)
298+
binding.customStatusInput.text?.clear()
299+
binding.customStatusInput.text?.append(predefinedStatus.message)
300+
301+
binding.remainingClearTime.visibility = View.GONE
302+
binding.clearStatusAfterSpinner.visibility = View.VISIBLE
303+
binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after)
304+
305+
val clearAt = predefinedStatus.clearAt
306+
if (clearAt == null) {
307+
binding.clearStatusAfterSpinner.setSelection(0)
308+
} else {
309+
when (clearAt.type) {
310+
CLEAR_AT_TYPE_PERIOD -> updateClearAtViewsForPeriod(clearAt)
311+
CLEAR_AT_TYPE_END_OF -> updateClearAtViewsForEndOf(clearAt)
312+
}
313+
}
314+
setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition)
315+
}
316+
317+
private fun updateClearAtViewsForPeriod(clearAt: ClearAt) {
318+
when (clearAt.time) {
319+
"1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR)
320+
"3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR)
321+
"14400" -> binding.clearStatusAfterSpinner.setSelection(POS_FOUR_HOURS)
322+
else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
323+
}
324+
}
325+
326+
private fun updateClearAtViewsForEndOf(clearAt: ClearAt) {
327+
when (clearAt.time) {
328+
"day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY)
329+
"week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK)
330+
else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
331+
}
332+
}
333+
334+
@VisibleForTesting
335+
fun setPredefinedStatus(predefinedStatus: ArrayList<PredefinedStatus>) {
336+
adapter.list = predefinedStatus
337+
binding.predefinedStatusList.adapter?.notifyDataSetChanged()
338+
}
339+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2020 Tobias Kaminsky <[email protected]>
5+
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
6+
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
7+
*/
8+
package it.niedermann.owncloud.notes.accountswitcher.adapter
9+
10+
import com.owncloud.android.lib.resources.users.PredefinedStatus
11+
12+
interface PredefinedStatusClickListener {
13+
fun onClick(predefinedStatus: PredefinedStatus)
14+
}

0 commit comments

Comments
 (0)