diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt index bb57b76751..e4dfb14d3f 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt @@ -59,10 +59,10 @@ import com.instructure.pandautils.compose.composables.Loading import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter import com.instructure.pandautils.features.dashboard.widget.WidgetMetadata import com.instructure.pandautils.features.dashboard.widget.courseinvitation.CourseInvitationsWidget +import com.instructure.pandautils.features.dashboard.widget.welcome.WelcomeWidget import com.instructure.pandautils.features.dashboard.widget.institutionalannouncements.InstitutionalAnnouncementsWidget import com.instructure.student.R import com.instructure.student.activity.NavigationActivity -import com.instructure.student.features.dashboard.widget.welcome.WelcomeWidget import kotlinx.coroutines.flow.SharedFlow @Composable diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index e3e761ce13..590a0883f9 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -2230,6 +2230,123 @@ Decline Invitation Are you sure you want to decline the invitation to %s? + + + Good morning, %s! + Good afternoon, %s! + Good evening, %s! + Good night, %s! + + + Good morning! + Good afternoon! + Good evening! + Good night! + + + + You\'ve got this. + Keep going — you\'re stronger than you feel right now. + One step at a time is still progress. + Don\'t give up — future you will thank you. + You\'re capable of more than you think. + Even on tough days, you\'re moving forward. + Trust yourself — you\'ve done hard things before. + Progress, not perfection. You\'re doing great. + Hang in there — you\'re not alone in this. + You\'re learning, growing, and doing better than you realize. + It\'s okay to stumble — you\'re still on the right path. + Keep pushing — you\'re closer than you think. + Small wins count too. + Keep going — you\'re closer than you think. + It\'s okay to pause. Breaks are part of learning. + Trying is already a win. + Progress > perfection. + Showing up matters more than you know. + You\'re building skills, even on slow days. + Don\'t forget to breathe — you\'re doing fine. + Your pace is the right pace. + Not everything needs to be figured out today. + You belong here. + Every effort you make adds up. + It\'s okay to start again — as many times as you need. + What feels hard now will feel easier later. + Keep showing up — that\'s what counts. + Small steps move big mountains. + Rest is part of progress too. + You\'re doing better than you realize. + Even slow progress is still progress. + The future isn\'t built in a day — but you\'re on the way. + One assignment, one moment, one step at a time. + You don\'t have to be perfect to make an impact. + Learning is messy — and that\'s normal. + Every try is growth, even if it doesn\'t feel like it. + You\'ve done hard things before — you can do this too. + Your effort matters, even if no one sees it. + It\'s okay to take things slow. + You\'re moving forward, even on quiet days. + The path doesn\'t need to be clear yet — keep walking. + You\'re stronger than you feel right now. + Big goals are built from small steps. + Keep going — future you will thank you. + Even messy progress is still progress. + You\'re not behind — you\'re on your path. + It\'s okay to learn as you go. + You\'re growing in ways you might not see yet. + Your effort today is an investment in tomorrow. + + + + Morning! You\'ve got this — one class, one step at a time. + Not feeling ready? That\'s normal. Just start where you are. + Coffee helps, but kindness to yourself works better. + Tech acting up? Happens to all of us — don\'t stress. + Today doesn\'t need to be perfect, just possible. + Good morning — today is a new chance to learn and grow. + Even small steps this morning move you closer to your goals. + Take a breath — you don\'t need to have everything figured out yet. + Technology can be tricky, but you\'re not alone in learning it. + + + + Halfway there — you\'ve already done more than you think. + Feeling stuck? Everyone hits walls, just don\'t stop climbing. + Jobs, grades, the future… no one has it all figured out yet. + Brain tired? Quick break = better focus later. + Ask for help. Seriously, no one\'s doing this solo. + You\'ve already made it this far today — that\'s something to be proud of. + Need a pause? Recharging is part of learning too. + It\'s okay if the path feels uncertain — skills build step by step. + Reach out if you\'re stuck — support is always closer than it feels. + + + + Made it through the day — that\'s a win in itself. + Missing people? Shoot someone a quick "hey" — it helps. + Even if today felt messy, you showed up. That matters. + Remember: no grade measures your worth. + Relax, laugh, or scroll guilt-free — you earned it. + Well done getting through the day — progress counts, even when it\'s quiet. + Missing friends or mentors? Connection can come in small moments too. + Evenings are for reflection — notice what you\'ve learned today, not just what\'s left to do. + Your effort matters more than perfection. + + + + Still grinding? Respect — but don\'t forget sleep exists. + Tomorrow you\'ll thank yourself for resting tonight. + Anxiety gets louder at night — don\'t believe all its noise. + You\'re not behind, you\'re just on your path. + Close the laptop — your brain needs dreams too. + It\'s okay to rest — tomorrow is waiting with new opportunities. + Learning is a marathon, not a sprint. Be kind to yourself tonight. + If worries feel heavy, remember you don\'t have to carry them alone. + End the day knowing that trying is already an achievement. + + + + Welcome message: %1$s. %2$s + Announcements (%d) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt index dbea27770a..4c4b89934b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt @@ -52,6 +52,7 @@ import org.threeten.bp.Clock import java.util.Locale import java.util.TimeZone import javax.inject.Singleton +import kotlin.random.Random /** * Module that provides all the application scope dependencies, that are not related to other module. @@ -189,4 +190,9 @@ class ApplicationModule { fun provideFileCache(): FileCache { return FileCache } + + @Provides + fun provideRandom(): Random { + return Random.Default + } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDay.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDay.kt new file mode 100644 index 0000000000..a5956121d2 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDay.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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, version 3 of the License. + * + * 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 . + */ + +package com.instructure.pandautils.features.dashboard.widget.welcome + +enum class TimeOfDay { + MORNING, // 4am - 12pm + AFTERNOON, // 12pm - 5pm + EVENING, // 5pm - 9pm + NIGHT // 9pm - 4am +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculator.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculator.kt new file mode 100644 index 0000000000..c44097f1e9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculator.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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, version 3 of the License. + * + * 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 . + */ + +package com.instructure.pandautils.features.dashboard.widget.welcome + +class TimeOfDayCalculator(private val timeProvider: TimeProvider) { + + fun getTimeOfDay(): TimeOfDay { + val hour = timeProvider.getCurrentHourOfDay() + return when { + hour < 4 -> TimeOfDay.NIGHT + hour < 12 -> TimeOfDay.MORNING + hour < 17 -> TimeOfDay.AFTERNOON + hour < 21 -> TimeOfDay.EVENING + else -> TimeOfDay.NIGHT + } + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeProvider.kt new file mode 100644 index 0000000000..7398c968bb --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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, version 3 of the License. + * + * 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 . + */ + +package com.instructure.pandautils.features.dashboard.widget.welcome + +import java.util.Calendar + +interface TimeProvider { + fun getCurrentHourOfDay(): Int +} + +class SystemTimeProvider : TimeProvider { + override fun getCurrentHourOfDay(): Int { + return Calendar.getInstance().get(Calendar.HOUR_OF_DAY) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidget.kt similarity index 70% rename from apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetScreen.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidget.kt index 08bf943659..4f37e79851 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidget.kt @@ -14,23 +14,27 @@ * along with this program. If not, see . */ -package com.instructure.student.features.dashboard.widget.welcome +package com.instructure.pandautils.features.dashboard.widget.welcome import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import com.instructure.student.R +import com.instructure.pandautils.R import kotlinx.coroutines.flow.SharedFlow @Composable @@ -41,6 +45,12 @@ fun WelcomeWidget( val viewModel: WelcomeWidgetViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsState() + LaunchedEffect(refreshSignal) { + refreshSignal.collect { + viewModel.refresh() + } + } + WelcomeContent( modifier = modifier, uiState = uiState @@ -52,7 +62,17 @@ private fun WelcomeContent( modifier: Modifier = Modifier, uiState: WelcomeWidgetUiState ) { - Column(modifier = modifier.padding(horizontal = 16.dp)) { + val contentDescriptionText = stringResource( + R.string.welcomeWidgetContentDescription, + uiState.greeting, + uiState.message + ) + + Column( + modifier = modifier + .padding(horizontal = 16.dp) + .semantics { contentDescription = contentDescriptionText } + ) { Text( modifier = Modifier.fillMaxWidth(), text = uiState.greeting, @@ -61,11 +81,15 @@ private fun WelcomeContent( color = colorResource(R.color.textDarkest), lineHeight = 29.sp ) - Text(modifier = Modifier.fillMaxWidth(), + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp), text = uiState.message, fontSize = 14.sp, color = colorResource(R.color.textDarkest), - lineHeight = 19.sp) + lineHeight = 19.sp + ) } } @@ -75,8 +99,8 @@ private fun WelcomeContent( fun WelcomeContentPreview() { WelcomeContent( uiState = WelcomeWidgetUiState( - greeting = "Welcome back, Student!", - message = "Here's what's happening in your courses today." + greeting = "Good morning, Riley!", + message = "Every small step you take is progress. Keep going!" ) ) -} \ No newline at end of file +} diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt similarity index 91% rename from apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt index e69994e2a6..91b28b47bd 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetUiState.kt @@ -14,9 +14,9 @@ * along with this program. If not, see . */ -package com.instructure.student.features.dashboard.widget.welcome +package com.instructure.pandautils.features.dashboard.widget.welcome data class WelcomeWidgetUiState( val greeting: String = "", val message: String = "" -) \ No newline at end of file +) diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt similarity index 62% rename from apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt index 3cd2e293e5..56166dbea9 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModel.kt @@ -14,9 +14,11 @@ * along with this program. If not, see . */ -package com.instructure.student.features.dashboard.widget.welcome +package com.instructure.pandautils.features.dashboard.widget.welcome import androidx.lifecycle.ViewModel +import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeGreetingUseCase +import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeMessageUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,17 +27,28 @@ import kotlinx.coroutines.flow.update import javax.inject.Inject @HiltViewModel -class WelcomeWidgetViewModel @Inject constructor() : ViewModel() { +class WelcomeWidgetViewModel @Inject constructor( + private val getWelcomeGreetingUseCase: GetWelcomeGreetingUseCase, + private val getWelcomeMessageUseCase: GetWelcomeMessageUseCase +) : ViewModel() { private val _uiState = MutableStateFlow(WelcomeWidgetUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { + loadWelcomeContent() + } + + fun refresh() { + loadWelcomeContent() + } + + private fun loadWelcomeContent() { _uiState.update { it.copy( - greeting = "Welcome back, Learner!", - message = "Here you can find an overview of your courses and activities." + greeting = getWelcomeGreetingUseCase(), + message = getWelcomeMessageUseCase() ) } } -} \ No newline at end of file +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/di/WelcomeWidgetModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/di/WelcomeWidgetModule.kt new file mode 100644 index 0000000000..d5e68f782a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/di/WelcomeWidgetModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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, version 3 of the License. + * + * 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 . + */ + +package com.instructure.pandautils.features.dashboard.widget.welcome.di + +import com.instructure.pandautils.features.dashboard.widget.welcome.SystemTimeProvider +import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDayCalculator +import com.instructure.pandautils.features.dashboard.widget.welcome.TimeProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class WelcomeWidgetModule { + + @Provides + fun provideTimeProvider(): TimeProvider = SystemTimeProvider() + + @Provides + fun provideTimeOfDayCalculator(timeProvider: TimeProvider): TimeOfDayCalculator { + return TimeOfDayCalculator(timeProvider) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeGreetingUseCase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeGreetingUseCase.kt new file mode 100644 index 0000000000..40937f7783 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeGreetingUseCase.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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, version 3 of the License. + * + * 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 . + */ + +package com.instructure.pandautils.features.dashboard.widget.welcome.usecase + +import android.content.res.Resources +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.R +import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDay +import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDayCalculator +import javax.inject.Inject + +class GetWelcomeGreetingUseCase @Inject constructor( + private val resources: Resources, + private val timeOfDayCalculator: TimeOfDayCalculator, + private val apiPrefs: ApiPrefs +) { + + operator fun invoke(): String { + val timeOfDay = timeOfDayCalculator.getTimeOfDay() + val firstName = apiPrefs.user?.shortName + + return if (!firstName.isNullOrBlank()) { + when (timeOfDay) { + TimeOfDay.MORNING -> resources.getString(R.string.welcomeGreetingMorningWithName, firstName) + TimeOfDay.AFTERNOON -> resources.getString(R.string.welcomeGreetingAfternoonWithName, firstName) + TimeOfDay.EVENING -> resources.getString(R.string.welcomeGreetingEveningWithName, firstName) + TimeOfDay.NIGHT -> resources.getString(R.string.welcomeGreetingNightWithName, firstName) + } + } else { + when (timeOfDay) { + TimeOfDay.MORNING -> resources.getString(R.string.welcomeGreetingMorning) + TimeOfDay.AFTERNOON -> resources.getString(R.string.welcomeGreetingAfternoon) + TimeOfDay.EVENING -> resources.getString(R.string.welcomeGreetingEvening) + TimeOfDay.NIGHT -> resources.getString(R.string.welcomeGreetingNight) + } + } + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeMessageUseCase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeMessageUseCase.kt new file mode 100644 index 0000000000..924bcd6e02 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/widget/welcome/usecase/GetWelcomeMessageUseCase.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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, version 3 of the License. + * + * 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 . + */ + +package com.instructure.pandautils.features.dashboard.widget.welcome.usecase + +import android.content.res.Resources +import com.instructure.pandautils.R +import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDay +import com.instructure.pandautils.features.dashboard.widget.welcome.TimeOfDayCalculator +import javax.inject.Inject +import kotlin.random.Random + +class GetWelcomeMessageUseCase @Inject constructor( + private val resources: Resources, + private val timeOfDayCalculator: TimeOfDayCalculator, + private val random: Random +) { + + operator fun invoke(): String { + val timeOfDay = timeOfDayCalculator.getTimeOfDay() + + val genericMessages = resources.getStringArray(R.array.welcomeMessagesGeneric) + val timeSpecificMessages = when (timeOfDay) { + TimeOfDay.MORNING -> resources.getStringArray(R.array.welcomeMessagesMorning) + TimeOfDay.AFTERNOON -> resources.getStringArray(R.array.welcomeMessagesAfternoon) + TimeOfDay.EVENING -> resources.getStringArray(R.array.welcomeMessagesEvening) + TimeOfDay.NIGHT -> resources.getStringArray(R.array.welcomeMessagesNight) + } + + val allMessages = genericMessages + timeSpecificMessages + return allMessages[random.nextInt(allMessages.size)] + } +} diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeGreetingUseCaseTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeGreetingUseCaseTest.kt new file mode 100644 index 0000000000..24bb05c231 --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeGreetingUseCaseTest.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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. + * + */ +package com.instructure.pandautils.features.dashboard.widget.welcome + +import android.content.res.Resources +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.R +import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeGreetingUseCase +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class GetWelcomeGreetingUseCaseTest { + + private val resources: Resources = mockk() + private val timeOfDayCalculator: TimeOfDayCalculator = mockk() + private val apiPrefs: ApiPrefs = mockk() + + private lateinit var useCase: GetWelcomeGreetingUseCase + + @Before + fun setUp() { + mockkObject(ApiPrefs) + useCase = GetWelcomeGreetingUseCase(resources, timeOfDayCalculator, apiPrefs) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `invoke returns morning greeting with name when user has short name`() { + val user = User(shortName = "Riley") + every { apiPrefs.user } returns user + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING + every { resources.getString(R.string.welcomeGreetingMorningWithName, "Riley") } returns "Good morning, Riley!" + + val result = useCase() + + assertEquals("Good morning, Riley!", result) + } + + @Test + fun `invoke returns afternoon greeting with name when user has short name`() { + val user = User(shortName = "Riley") + every { apiPrefs.user } returns user + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.AFTERNOON + every { resources.getString(R.string.welcomeGreetingAfternoonWithName, "Riley") } returns "Good afternoon, Riley!" + + val result = useCase() + + assertEquals("Good afternoon, Riley!", result) + } + + @Test + fun `invoke returns evening greeting with name when user has short name`() { + val user = User(shortName = "Riley") + every { apiPrefs.user } returns user + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.EVENING + every { resources.getString(R.string.welcomeGreetingEveningWithName, "Riley") } returns "Good evening, Riley!" + + val result = useCase() + + assertEquals("Good evening, Riley!", result) + } + + @Test + fun `invoke returns night greeting with name when user has short name`() { + val user = User(shortName = "Riley") + every { apiPrefs.user } returns user + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.NIGHT + every { resources.getString(R.string.welcomeGreetingNightWithName, "Riley") } returns "Good night, Riley!" + + val result = useCase() + + assertEquals("Good night, Riley!", result) + } + + @Test + fun `invoke returns morning greeting without name when user has null short name`() { + val user = User(shortName = null) + every { apiPrefs.user } returns user + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING + every { resources.getString(R.string.welcomeGreetingMorning) } returns "Good morning!" + + val result = useCase() + + assertEquals("Good morning!", result) + } + + @Test + fun `invoke returns morning greeting without name when user has blank short name`() { + val user = User(shortName = " ") + every { apiPrefs.user } returns user + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING + every { resources.getString(R.string.welcomeGreetingMorning) } returns "Good morning!" + + val result = useCase() + + assertEquals("Good morning!", result) + } + + @Test + fun `invoke returns morning greeting without name when user is null`() { + every { apiPrefs.user } returns null + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING + every { resources.getString(R.string.welcomeGreetingMorning) } returns "Good morning!" + + val result = useCase() + + assertEquals("Good morning!", result) + } + + @Test + fun `invoke returns afternoon greeting without name when user is null`() { + every { apiPrefs.user } returns null + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.AFTERNOON + every { resources.getString(R.string.welcomeGreetingAfternoon) } returns "Good afternoon!" + + val result = useCase() + + assertEquals("Good afternoon!", result) + } + + @Test + fun `invoke returns evening greeting without name when user is null`() { + every { apiPrefs.user } returns null + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.EVENING + every { resources.getString(R.string.welcomeGreetingEvening) } returns "Good evening!" + + val result = useCase() + + assertEquals("Good evening!", result) + } + + @Test + fun `invoke returns night greeting without name when user is null`() { + every { apiPrefs.user } returns null + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.NIGHT + every { resources.getString(R.string.welcomeGreetingNight) } returns "Good night!" + + val result = useCase() + + assertEquals("Good night!", result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeMessageUseCaseTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeMessageUseCaseTest.kt new file mode 100644 index 0000000000..b6d1efa130 --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/GetWelcomeMessageUseCaseTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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. + * + */ +package com.instructure.pandautils.features.dashboard.widget.welcome + +import android.content.res.Resources +import com.instructure.pandautils.R +import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeMessageUseCase +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.random.Random + +class GetWelcomeMessageUseCaseTest { + + private val resources: Resources = mockk() + private val timeOfDayCalculator: TimeOfDayCalculator = mockk() + private val random: Random = mockk() + + private val useCase = GetWelcomeMessageUseCase(resources, timeOfDayCalculator, random) + + @Test + fun `invoke returns first generic message when random returns 0 and time is morning`() { + val genericMessages = arrayOf("Generic message 1", "Generic message 2") + val morningMessages = arrayOf("Morning message 1", "Morning message 2") + + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING + every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages + every { resources.getStringArray(R.array.welcomeMessagesMorning) } returns morningMessages + every { random.nextInt(4) } returns 0 + + val result = useCase() + + assertEquals("Generic message 1", result) + } + + @Test + fun `invoke returns first morning-specific message when random returns 2 and time is morning`() { + val genericMessages = arrayOf("Generic message 1", "Generic message 2") + val morningMessages = arrayOf("Morning message 1", "Morning message 2") + + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.MORNING + every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages + every { resources.getStringArray(R.array.welcomeMessagesMorning) } returns morningMessages + every { random.nextInt(4) } returns 2 + + val result = useCase() + + assertEquals("Morning message 1", result) + } + + @Test + fun `invoke returns afternoon-specific message when random returns 3 and time is afternoon`() { + val genericMessages = arrayOf("Generic message 1", "Generic message 2") + val afternoonMessages = arrayOf("Afternoon message 1", "Afternoon message 2") + + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.AFTERNOON + every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages + every { resources.getStringArray(R.array.welcomeMessagesAfternoon) } returns afternoonMessages + every { random.nextInt(4) } returns 3 + + val result = useCase() + + assertEquals("Afternoon message 2", result) + } + + @Test + fun `invoke returns evening-specific message when random returns 2 and time is evening`() { + val genericMessages = arrayOf("Generic message 1", "Generic message 2") + val eveningMessages = arrayOf("Evening message 1", "Evening message 2") + + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.EVENING + every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages + every { resources.getStringArray(R.array.welcomeMessagesEvening) } returns eveningMessages + every { random.nextInt(4) } returns 2 + + val result = useCase() + + assertEquals("Evening message 1", result) + } + + @Test + fun `invoke returns night-specific message when random returns 3 and time is night`() { + val genericMessages = arrayOf("Generic message 1", "Generic message 2") + val nightMessages = arrayOf("Night message 1", "Night message 2") + + every { timeOfDayCalculator.getTimeOfDay() } returns TimeOfDay.NIGHT + every { resources.getStringArray(R.array.welcomeMessagesGeneric) } returns genericMessages + every { resources.getStringArray(R.array.welcomeMessagesNight) } returns nightMessages + every { random.nextInt(4) } returns 3 + + val result = useCase() + + assertEquals("Night message 2", result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculatorTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculatorTest.kt new file mode 100644 index 0000000000..6ff12064d5 --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/TimeOfDayCalculatorTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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. + * + */ +package com.instructure.pandautils.features.dashboard.widget.welcome + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test + +class TimeOfDayCalculatorTest { + + private val timeProvider: TimeProvider = mockk() + private val calculator = TimeOfDayCalculator(timeProvider) + + @Test + fun `getTimeOfDay returns NIGHT when hour is 0`() { + every { timeProvider.getCurrentHourOfDay() } returns 0 + assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns NIGHT when hour is 3`() { + every { timeProvider.getCurrentHourOfDay() } returns 3 + assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns MORNING when hour is 4`() { + every { timeProvider.getCurrentHourOfDay() } returns 4 + assertEquals(TimeOfDay.MORNING, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns MORNING when hour is 8`() { + every { timeProvider.getCurrentHourOfDay() } returns 8 + assertEquals(TimeOfDay.MORNING, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns MORNING when hour is 11`() { + every { timeProvider.getCurrentHourOfDay() } returns 11 + assertEquals(TimeOfDay.MORNING, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns AFTERNOON when hour is 12`() { + every { timeProvider.getCurrentHourOfDay() } returns 12 + assertEquals(TimeOfDay.AFTERNOON, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns AFTERNOON when hour is 14`() { + every { timeProvider.getCurrentHourOfDay() } returns 14 + assertEquals(TimeOfDay.AFTERNOON, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns AFTERNOON when hour is 16`() { + every { timeProvider.getCurrentHourOfDay() } returns 16 + assertEquals(TimeOfDay.AFTERNOON, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns EVENING when hour is 17`() { + every { timeProvider.getCurrentHourOfDay() } returns 17 + assertEquals(TimeOfDay.EVENING, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns EVENING when hour is 19`() { + every { timeProvider.getCurrentHourOfDay() } returns 19 + assertEquals(TimeOfDay.EVENING, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns EVENING when hour is 20`() { + every { timeProvider.getCurrentHourOfDay() } returns 20 + assertEquals(TimeOfDay.EVENING, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns NIGHT when hour is 21`() { + every { timeProvider.getCurrentHourOfDay() } returns 21 + assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns NIGHT when hour is 22`() { + every { timeProvider.getCurrentHourOfDay() } returns 22 + assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay()) + } + + @Test + fun `getTimeOfDay returns NIGHT when hour is 23`() { + every { timeProvider.getCurrentHourOfDay() } returns 23 + assertEquals(TimeOfDay.NIGHT, calculator.getTimeOfDay()) + } +} diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModelTest.kt new file mode 100644 index 0000000000..ca228f5ed6 --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/widget/welcome/WelcomeWidgetViewModelTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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. + * + */ +package com.instructure.pandautils.features.dashboard.widget.welcome + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeGreetingUseCase +import com.instructure.pandautils.features.dashboard.widget.welcome.usecase.GetWelcomeMessageUseCase +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class WelcomeWidgetViewModelTest { + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + + private val testDispatcher = UnconfinedTestDispatcher() + private val getWelcomeGreetingUseCase: GetWelcomeGreetingUseCase = mockk() + private val getWelcomeMessageUseCase: GetWelcomeMessageUseCase = mockk() + + private lateinit var viewModel: WelcomeWidgetViewModel + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `init loads greeting and message`() { + every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!" + every { getWelcomeMessageUseCase() } returns "Every small step you take is progress." + + viewModel = createViewModel() + + val state = viewModel.uiState.value + assertEquals("Good morning, Riley!", state.greeting) + assertEquals("Every small step you take is progress.", state.message) + verify(exactly = 1) { getWelcomeGreetingUseCase() } + verify(exactly = 1) { getWelcomeMessageUseCase() } + } + + @Test + fun `init loads greeting without name when user has no short name`() { + every { getWelcomeGreetingUseCase() } returns "Good morning!" + every { getWelcomeMessageUseCase() } returns "Start your day with purpose." + + viewModel = createViewModel() + + val state = viewModel.uiState.value + assertEquals("Good morning!", state.greeting) + assertEquals("Start your day with purpose.", state.message) + } + + @Test + fun `init loads afternoon greeting`() { + every { getWelcomeGreetingUseCase() } returns "Good afternoon, Riley!" + every { getWelcomeMessageUseCase() } returns "Keep up the great work." + + viewModel = createViewModel() + + val state = viewModel.uiState.value + assertEquals("Good afternoon, Riley!", state.greeting) + assertEquals("Keep up the great work.", state.message) + } + + @Test + fun `init loads evening greeting`() { + every { getWelcomeGreetingUseCase() } returns "Good evening, Riley!" + every { getWelcomeMessageUseCase() } returns "Finish strong today." + + viewModel = createViewModel() + + val state = viewModel.uiState.value + assertEquals("Good evening, Riley!", state.greeting) + assertEquals("Finish strong today.", state.message) + } + + @Test + fun `init loads night greeting`() { + every { getWelcomeGreetingUseCase() } returns "Good night, Riley!" + every { getWelcomeMessageUseCase() } returns "Rest well, you earned it." + + viewModel = createViewModel() + + val state = viewModel.uiState.value + assertEquals("Good night, Riley!", state.greeting) + assertEquals("Rest well, you earned it.", state.message) + } + + @Test + fun `refresh updates greeting and message`() { + every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!" + every { getWelcomeMessageUseCase() } returns "First message" + + viewModel = createViewModel() + + val initialState = viewModel.uiState.value + assertEquals("Good morning, Riley!", initialState.greeting) + assertEquals("First message", initialState.message) + + every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!" + every { getWelcomeMessageUseCase() } returns "Second message" + + viewModel.refresh() + + val refreshedState = viewModel.uiState.value + assertEquals("Good morning, Riley!", refreshedState.greeting) + assertEquals("Second message", refreshedState.message) + verify(exactly = 2) { getWelcomeGreetingUseCase() } + verify(exactly = 2) { getWelcomeMessageUseCase() } + } + + @Test + fun `refresh updates greeting when time changes`() { + every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!" + every { getWelcomeMessageUseCase() } returns "Morning message" + + viewModel = createViewModel() + + val initialState = viewModel.uiState.value + assertEquals("Good morning, Riley!", initialState.greeting) + + every { getWelcomeGreetingUseCase() } returns "Good afternoon, Riley!" + every { getWelcomeMessageUseCase() } returns "Afternoon message" + + viewModel.refresh() + + val refreshedState = viewModel.uiState.value + assertEquals("Good afternoon, Riley!", refreshedState.greeting) + assertEquals("Afternoon message", refreshedState.message) + } + + @Test + fun `multiple refresh calls update state correctly`() { + every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!" + every { getWelcomeMessageUseCase() } returnsMany listOf( + "Message 1", + "Message 2", + "Message 3" + ) + + viewModel = createViewModel() + + assertEquals("Message 1", viewModel.uiState.value.message) + + viewModel.refresh() + assertEquals("Message 2", viewModel.uiState.value.message) + + viewModel.refresh() + assertEquals("Message 3", viewModel.uiState.value.message) + + verify(exactly = 3) { getWelcomeGreetingUseCase() } + verify(exactly = 3) { getWelcomeMessageUseCase() } + } + + @Test + fun `uiState initial values are empty strings`() { + every { getWelcomeGreetingUseCase() } returns "Good morning, Riley!" + every { getWelcomeMessageUseCase() } returns "Test message" + + // Create ViewModel but check state before init completes would show empty strings + // However, since init runs immediately, we verify the pattern is correct + viewModel = createViewModel() + + // After init, state should be populated + val state = viewModel.uiState.value + assertEquals("Good morning, Riley!", state.greeting) + assertEquals("Test message", state.message) + } + + private fun createViewModel(): WelcomeWidgetViewModel { + return WelcomeWidgetViewModel( + getWelcomeGreetingUseCase = getWelcomeGreetingUseCase, + getWelcomeMessageUseCase = getWelcomeMessageUseCase + ) + } +} \ No newline at end of file