Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APPS-3549: Announcements UI/Teachers #947

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ import org.stepik.android.domain.course_news.model.CourseNewsListItem
interface CourseNewsFeature {
sealed class State {
data class Idle(val mustFetchRemote: Boolean = false) : State()
data class Empty(val announcementIds: List<Long>) : State()
data class Empty(val announcementIds: List<Long>, val isTeacher: Boolean) : State()
object NotEnrolled : State()
object Error : State()
data class LoadingAnnouncements(val announcementIds: List<Long>, val sourceType: DataSourceType) : State()
data class LoadingAnnouncements(val announcementIds: List<Long>, val isTeacher: Boolean, val sourceType: DataSourceType) : State()
data class Content(
val announcementIds: List<Long>,
val courseNewsListItems: List<CourseNewsListItem.Data>,
val isTeacher: Boolean,
val sourceType: DataSourceType, // Necessary for next page loading
val isLoadingRemote: Boolean, // Needed to block next page loading, when cache is loaded and we are loading remote
val isLoadingNextPage: Boolean
) : State()
}

sealed class Message {
data class InitMessage(val announcementIds: List<Long>) : Message()
data class InitMessage(val announcementIds: List<Long>, val isTeacher: Boolean) : Message()
object OnScreenOpenedMessage : Message()

data class FetchAnnouncementIdsFailure(val throwable: Throwable) : Message()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ constructor(
.getCourse()
.map { course ->
if (course.enrollment != 0L) {
Result.success(course.announcements?.sortedDescending() ?: emptyList())
Result.success(course)
} else {
Result.failure(NotEnrolledException())
}
Expand All @@ -66,7 +66,14 @@ constructor(
.subscribeBy(
onNext = { result ->
result.fold(
onSuccess = { onNewMessage(CourseNewsFeature.Message.InitMessage(it)) },
onSuccess = { course ->
onNewMessage(
CourseNewsFeature.Message.InitMessage(
course.announcements?.sortedDescending() ?: emptyList(),
course.actions?.createAnnouncements != null
)
)
},
onFailure = { onNewMessage(CourseNewsFeature.Message.FetchAnnouncementIdsFailure(it)) }
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ constructor() : StateReducer<State, Message, Action> {
val fetchAnnouncementIds = message.announcementIds.slice(0, PAGE_SIZE)

if (fetchAnnouncementIds.isEmpty()) {
State.Empty(message.announcementIds) to emptySet()
State.Empty(message.announcementIds, message.isTeacher) to emptySet()
} else {
State.LoadingAnnouncements(message.announcementIds, sourceType) to setOf(Action.FetchAnnouncements(fetchAnnouncementIds, sourceType))
State.LoadingAnnouncements(message.announcementIds, message.isTeacher, sourceType) to setOf(Action.FetchAnnouncements(fetchAnnouncementIds, sourceType))
}
}
is Message.OnScreenOpenedMessage -> {
Expand All @@ -41,12 +41,12 @@ constructor() : StateReducer<State, Message, Action> {
State.Idle(mustFetchRemote = true) to emptySet()
}
is State.Empty -> {
State.LoadingAnnouncements(state.announcementIds, DataSourceType.REMOTE) to
State.LoadingAnnouncements(state.announcementIds, state.isTeacher, DataSourceType.REMOTE) to
setOf(Action.FetchAnnouncements(state.announcementIds.slice(0, PAGE_SIZE), DataSourceType.REMOTE))
}
is State.LoadingAnnouncements -> {
if (state.sourceType == DataSourceType.CACHE) {
State.LoadingAnnouncements(state.announcementIds, DataSourceType.REMOTE) to
State.LoadingAnnouncements(state.announcementIds, state.isTeacher, DataSourceType.REMOTE) to
setOf(Action.FetchAnnouncements(state.announcementIds.slice(0, PAGE_SIZE), DataSourceType.REMOTE))
} else {
null
Expand Down Expand Up @@ -79,11 +79,12 @@ constructor() : StateReducer<State, Message, Action> {
when (state) {
is State.LoadingAnnouncements -> {
if (message.courseNewsListItems.isEmpty()) {
State.Empty(state.announcementIds) to emptySet()
State.Empty(state.announcementIds, state.isTeacher) to emptySet()
} else {
State.Content(
state.announcementIds,
message.courseNewsListItems,
state.isTeacher,
state.sourceType,
isLoadingRemote = false,
isLoadingNextPage = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.stepik.android.view.course_news.model

import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.stepic.droid.R

enum class AnnouncementBadge(
@DrawableRes
val backgroundRes: Int,

@ColorRes
val textColorRes: Int,

@DrawableRes
val compoundDrawableRes: Int,

@StringRes
val textRes: Int
) {
COMPOSING(
backgroundRes = R.drawable.bg_announcement_composing,
textColorRes = R.color.color_on_surface_emphasis_medium,
compoundDrawableRes = R.drawable.ic_announcement_badge_composing,
textRes = R.string.course_news_composing_badge
),
SCHEDULED(
backgroundRes = R.drawable.bg_announcement_scheduled,
textColorRes = R.color.color_overlay_violet,
compoundDrawableRes = R.drawable.ic_announcement_badge_scheduled,
textRes = R.string.course_news_scheduled_badge
),
SENDING(
backgroundRes = R.drawable.bg_announcement_scheduled,
textColorRes = R.color.color_overlay_violet,
compoundDrawableRes = R.drawable.ic_announcement_badge_sending,
textRes = R.string.course_news_sending_badge
),
SENT(
backgroundRes = R.drawable.bg_announcement_sent,
textColorRes = R.color.color_overlay_green,
compoundDrawableRes = R.drawable.ic_announcement_badge_sent,
textRes = R.string.course_news_sent_badge
),
ON_EVENT(
backgroundRes = R.drawable.bg_announcement_on_event,
textColorRes = R.color.color_overlay_blue,
compoundDrawableRes = -1,
textRes = R.string.course_news_on_event_badge
),
ONE_TIME(
backgroundRes = R.drawable.bg_announcement_on_event,
textColorRes = R.color.color_overlay_blue,
compoundDrawableRes = -1,
textRes = R.string.course_news_one_time_badge
)
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
package org.stepik.android.view.course_news.ui.adapter.delegate

import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import by.kirich1409.viewbindingdelegate.viewBinding
import org.stepic.droid.R
import org.stepic.droid.databinding.ItemAnnouncementBadgeBinding
import org.stepic.droid.databinding.ItemCourseNewsBinding
import org.stepic.droid.ui.util.setCompoundDrawables
import org.stepic.droid.util.DateTimeHelper
import org.stepik.android.domain.announcement.model.Announcement
import org.stepik.android.domain.course_news.model.CourseNewsListItem
import org.stepik.android.view.course_news.model.AnnouncementBadge
import ru.nobird.android.ui.adapterdelegates.AdapterDelegate
import ru.nobird.android.ui.adapterdelegates.DelegateViewHolder
import ru.nobird.android.ui.adapterdelegates.dsl.adapterDelegate
import ru.nobird.android.ui.adapters.DefaultDelegateAdapter
import java.util.TimeZone

class CourseNewsAdapterDelegate : AdapterDelegate<CourseNewsListItem, DelegateViewHolder<CourseNewsListItem>>() {
class CourseNewsAdapterDelegate(
val isTeacher: Boolean
) : AdapterDelegate<CourseNewsListItem, DelegateViewHolder<CourseNewsListItem>>() {
override fun isForViewType(position: Int, data: CourseNewsListItem): Boolean =
data is CourseNewsListItem.Data

Expand All @@ -21,16 +34,124 @@ class CourseNewsAdapterDelegate : AdapterDelegate<CourseNewsListItem, DelegateVi

private inner class ViewHolder(root: View) : DelegateViewHolder<CourseNewsListItem>(root) {
private val viewBinding: ItemCourseNewsBinding by viewBinding { ItemCourseNewsBinding.bind(root) }
private val badgesAdapter = DefaultDelegateAdapter<AnnouncementBadge>()

init {
badgesAdapter += adapterDelegate(
layoutResId = R.layout.item_announcement_badge
) {
val badgesBinding: ItemAnnouncementBadgeBinding = ItemAnnouncementBadgeBinding.bind(this.itemView)
onBind {
val textColor = ContextCompat.getColor(context, it.textColorRes)
badgesBinding.root.setText(it.textRes)
badgesBinding.root.setTextColor(textColor)
badgesBinding.root.setBackgroundResource(it.backgroundRes)
badgesBinding.root.setCompoundDrawables(start = it.compoundDrawableRes)
}
}
}

override fun onBind(data: CourseNewsListItem) {
data as CourseNewsListItem.Data

val formattedDate = data.announcement.sentDate?.let { DateTimeHelper.getPrintableDate(it, DateTimeHelper.DISPLAY_DATETIME_PATTERN, TimeZone.getDefault()) }
with(viewBinding.newsBadges) {
itemAnimator = null
isNestedScrollingEnabled = false
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = badgesAdapter
}

viewBinding.newsBadges.isVisible = isTeacher
badgesAdapter.items = buildBadgesList(data.announcement)

val formattedDate = data.announcement.sentDate?.let {
DateTimeHelper.getPrintableDate(
it,
DateTimeHelper.DISPLAY_DATETIME_PATTERN,
TimeZone.getDefault()
)
}

viewBinding.newsDate.text = formattedDate
viewBinding.newsDate.isVisible = formattedDate != null

viewBinding.newsSubject.text = data.announcement.subject
viewBinding.newsText.setText(data.announcement.text)

val mustShowStatistics = isTeacher &&
(data.announcement.status == Announcement.AnnouncementStatus.SENDING ||
data.announcement.status == Announcement.AnnouncementStatus.SENT)

val teacherInformation =
if (mustShowStatistics) {
buildSpannedString {
data.announcement.publishCount?.let {
appendCount(this, R.string.course_news_publish_count, it)
}
data.announcement.queueCount?.let {
appendCount(this, R.string.course_news_queued_count, it)
}
data.announcement.sentCount?.let {
appendCount(this, R.string.course_news_sent_count, it)
}
data.announcement.openCount?.let {
appendCount(this, R.string.course_news_open_count, it)
}
data.announcement.clickCount?.let {
appendCount(this, R.string.course_news_click_count, it, newline = false)
}
}
} else {
""
}

viewBinding.newsStatistics.text = teacherInformation
viewBinding.newsStatistics.isVisible = teacherInformation.isNotEmpty()
}

private fun appendCount(spannableStringBuilder: SpannableStringBuilder, stringRes: Int, count: Int, newline: Boolean = true) {
with(spannableStringBuilder) {
append(context.getString(stringRes))
bold { append(count.toString()) }
if (newline) append("\n")
}
}

private fun buildBadgesList(announcement: Announcement): List<AnnouncementBadge> {
val isOneTimeEvent = !announcement.isInfinite && !announcement.onEnroll
val isActiveEvent = announcement.onEnroll ||
(announcement.isInfinite && (announcement.startDate == null || announcement.startDate.time < DateTimeHelper.nowUtc()))

val statusBadge =
when (announcement.status) {
Announcement.AnnouncementStatus.COMPOSING ->
AnnouncementBadge.COMPOSING

Announcement.AnnouncementStatus.SCHEDULED ->
if (isActiveEvent) {
AnnouncementBadge.SENDING
} else {
AnnouncementBadge.SCHEDULED
}

Announcement.AnnouncementStatus.QUEUEING,
Announcement.AnnouncementStatus.QUEUED,
Announcement.AnnouncementStatus.SENDING ->
AnnouncementBadge.SENDING

Announcement.AnnouncementStatus.SENT,
Announcement.AnnouncementStatus.ABORTED ->
AnnouncementBadge.SENT
}

val eventBadge =
if (isOneTimeEvent) {
AnnouncementBadge.ONE_TIME
} else {
AnnouncementBadge.ON_EVENT
}

return listOf(statusBadge, eventBadge)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ class CourseNewsFragment : Fragment(R.layout.fragment_course_news),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injectComponent(courseId)
courseNewsAdapter += adapterDelegate<CourseNewsListItem, CourseNewsListItem.Placeholder>(layoutResId = R.layout.item_course_news_placeholder)
courseNewsAdapter += CourseNewsAdapterDelegate()
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -105,13 +103,15 @@ class CourseNewsFragment : Fragment(R.layout.fragment_course_news),
is CourseNewsFeature.State.NotEnrolled ->
courseNewsBinding.courseNewsEmpty.placeholderMessage.text = getString(R.string.course_news_not_enrolled_message)
is CourseNewsFeature.State.LoadingAnnouncements -> {
handleDelegateAdapters(state.isTeacher)
courseNewsAdapter.items = listOf(
CourseNewsListItem.Placeholder,
CourseNewsListItem.Placeholder,
CourseNewsListItem.Placeholder
)
}
is CourseNewsFeature.State.Content -> {
handleDelegateAdapters(state.isTeacher)
if (state.isLoadingNextPage) {
courseNewsAdapter.items = state.courseNewsListItems + CourseNewsListItem.Placeholder
} else {
Expand All @@ -125,4 +125,10 @@ class CourseNewsFragment : Fragment(R.layout.fragment_course_news),
releaseComponent(courseId)
super.onDestroy()
}

private fun handleDelegateAdapters(isTeacher: Boolean) {
if (courseNewsAdapter.delegates.isNotEmpty()) return
courseNewsAdapter += adapterDelegate<CourseNewsListItem, CourseNewsListItem.Placeholder>(layoutResId = R.layout.item_course_news_placeholder)
courseNewsAdapter += CourseNewsAdapterDelegate(isTeacher)
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/bg_announcement_composing.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_elevation_overlay_1dp" />
<corners android:radius="16dp" />
</shape>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/bg_announcement_on_event.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_overlay_blue_alpha_12" />
<corners android:radius="4dp" />
</shape>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/bg_announcement_scheduled.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_overlay_violet_alpha_12" />
<corners android:radius="16dp" />
</shape>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/bg_announcement_sent.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_overlay_green_alpha_12" />
<corners android:radius="16dp" />
</shape>
7 changes: 7 additions & 0 deletions app/src/main/res/drawable/bg_item_course_news.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/color_on_surface_alpha_12" />
<corners android:radius="@dimen/corner_radius" />
</shape>
11 changes: 11 additions & 0 deletions app/src/main/res/drawable/ic_announcement_badge_composing.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M6.0021,3.6371C7.1316,3.6371 8.0483,4.5538 8.0483,5.6833C8.0483,5.9494 7.9952,6.199 7.9011,6.4323L9.0962,7.6274C9.714,7.1117 10.2011,6.4446 10.5,5.6833C9.7919,3.8867 8.0443,2.6138 5.9979,2.6138C5.4249,2.6138 4.8766,2.7162 4.369,2.9004L5.2531,3.7843C5.4864,3.6902 5.736,3.6371 6.0021,3.6371ZM1.9093,2.5197L3.0307,3.6411C2.3514,4.1691 1.8194,4.873 1.5,5.6833C2.2082,7.4802 3.9557,8.7529 6.0021,8.7529C6.6364,8.7529 7.242,8.6302 7.7947,8.4091L7.9665,8.5811L9.1658,9.7762L9.6854,9.2563L2.4292,2L1.9093,2.5197ZM4.1725,4.7829L4.807,5.4175C4.7865,5.5033 4.7743,5.5934 4.7743,5.6833C4.7743,6.3627 5.3227,6.9112 6.0021,6.9112C6.092,6.9112 6.1821,6.8989 6.268,6.8784L6.9025,7.5129C6.6282,7.6479 6.3253,7.7298 6.0021,7.7298C4.8724,7.7298 3.9557,6.813 3.9557,5.6833C3.9557,5.3601 4.0375,5.0573 4.1725,4.7829ZM5.9365,4.4637L7.2257,5.7529L7.234,5.6876C7.234,5.0082 6.6855,4.4597 6.0061,4.4597L5.9365,4.4637Z"
android:fillColor="@color/color_on_surface_emphasis_medium"
android:fillAlpha="0.6"
android:fillType="evenOdd"/>
</vector>
Loading