[Messages] Add support for starring messages. (#86)

* [UI/Messages] Add stars to message list layout.

* [Messages] Move text styling-related code to a manager.

* [Messages] Implement starring messages. Move more code to the manager. Update UI.

* [UI] Add padding to the no data text.

* [Messages] Fix checking sent message recipient read state.

* [Messages] Add star icon padding.
This commit is contained in:
Kuba Szczodrzyński 2021-10-08 21:43:11 +02:00 committed by GitHub
parent a6aca42c8c
commit 44263ac95f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 616 additions and 426 deletions

View File

@ -72,6 +72,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(this) }
val textStylingManager by lazy { TextStylingManager(this) }
val messageManager by lazy { MessageManager(this) }
val db
get() = App.db

View File

@ -814,6 +814,8 @@ fun View.attachToastHint(stringRes: Int) = onLongClick {
true
}
fun View.detachToastHint() = setOnLongClickListener(null)
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
observe(lifecycleOwner, object : Observer<T> {
override fun onChanged(t: T?) {

View File

@ -29,7 +29,7 @@ abstract class MessageDao : BaseDao<Message, MessageFull> {
LEFT JOIN metadata ON messageId = thingId AND thingType = ${Metadata.TYPE_MESSAGE} AND metadata.profileId = messages.profileId
"""
private const val ORDER_BY = """ORDER BY messageIsPinned, addedDate DESC"""
private const val ORDER_BY = """ORDER BY messageIsPinned DESC, addedDate DESC"""
}
private val selective by lazy { MessageDaoSelective(App.db) }

View File

@ -41,7 +41,7 @@ open class Message(
}
@ColumnInfo(name = "messageIsPinned")
var isPinned: Boolean = false
var isStarred: Boolean = false
var hasAttachments = false // if the attachments are not yet downloaded but we already know there are some
get() = field || attachmentIds.isNotNullNorEmpty()

View File

@ -31,6 +31,8 @@ class MessageFull(
var filterWeight = 0
@Ignore
var searchHighlightText: CharSequence? = null
@Ignore
var readByEveryone = true
// metadata
var seen = false

View File

@ -27,7 +27,6 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore.Companion.LOGIN_TYPE_IDZIENNIK
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
@ -53,6 +52,8 @@ class MessageFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val manager
get() = app.messageManager
private lateinit var message: MessageFull
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -92,6 +93,35 @@ class MessageFragment : Fragment(), CoroutineScope {
it.maxLines = if (it.maxLines == 30) 2 else 30
}
val replyDrawable = IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_reply_outline).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
val forwardDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_arrow_right).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
val deleteDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_delete_outline).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
val downloadDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_download_outline).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
b.replyButton.setCompoundDrawables(null, replyDrawable, null, null)
b.forwardButton.setCompoundDrawables(null, forwardDrawable, null, null)
b.deleteButton.setCompoundDrawables(null, deleteDrawable, null, null)
b.downloadButton.setCompoundDrawables(null, downloadDrawable, null, null)
b.messageStar.onClick {
launch {
manager.starMessage(message, !message.isStarred)
manager.setStarIcon(b.messageStar, message)
}
}
b.messageStar.attachToastHint(R.string.hint_message_star)
b.replyButton.onClick {
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE, Bundle(
"message" to app.gson.toJson(message),
@ -110,10 +140,7 @@ class MessageFragment : Fragment(), CoroutineScope {
.setMessage(R.string.messages_delete_confirmation_text)
.setPositiveButton(R.string.ok) { _, _ ->
launch {
message.type = TYPE_DELETED
withContext(Dispatchers.Default) {
app.db.messageDao().replace(message)
}
manager.markAsDeleted(message)
Toast.makeText(activity, "Wiadomość przeniesiona do usuniętych", Toast.LENGTH_SHORT).show()
activity.navigateUp()
}
@ -127,59 +154,10 @@ class MessageFragment : Fragment(), CoroutineScope {
}
launch {
val messageString = arguments?.getString("message")
val messageId = arguments?.getLong("messageId")
if (messageId == null) {
message = manager.getMessage(App.profileId, arguments) ?: run {
activity.navigateUp()
return@launch
}
val msg = withContext(Dispatchers.Default) {
val msg =
if (messageString != null)
app.gson.fromJson(messageString, MessageFull::class.java)?.also {
if (arguments?.getLong("sentDate") ?: 0L > 0L)
it.addedDate = arguments?.getLong("sentDate") ?: 0L
}
else
app.db.messageDao().getByIdNow(App.profileId, messageId)
if (msg == null)
return@withContext null
// make recipients ID-unique
// this helps when multiple profiles receive the same message
// (there are multiple -1 recipients for the same message ID)
val recipientsDistinct =
msg.recipients?.distinctBy { it.id } ?: return@withContext null
msg.recipients?.clear()
msg.recipients?.addAll(recipientsDistinct)
// load recipients in sent messages
val teachers by lazy { app.db.teacherDao().getAllNow(App.profileId) }
msg.recipients?.forEach { recipient ->
if (recipient.fullName == null) {
recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: ""
}
}
if (msg.type == TYPE_SENT && msg.senderName == null) {
msg.senderName = app.profile.accountName ?: app.profile.studentNameLong
}
//msg.recipients = app.db.messageRecipientDao().getAllByMessageId(msg.profileId, msg.id)
if (msg.body != null && !msg.seen) {
app.db.metadataDao().setSeen(msg.profileId, msg, true)
}
return@withContext msg
} ?: run {
activity.navigateUp()
return@launch
}
message = msg
b.subject.text = message.subject
checkMessage()
}
@ -208,7 +186,6 @@ class MessageFragment : Fragment(), CoroutineScope {
}
}
val readByAll = checkRecipients()
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_VULCAN) {
// vulcan: change message status or download attachments
if (message.type == TYPE_RECEIVED && !message.seen || message.attachmentIds == null) {
@ -216,7 +193,7 @@ class MessageFragment : Fragment(), CoroutineScope {
return
}
}
else if (!readByAll) {
else if (!message.readByEveryone) {
// if a sent msg is not read by everyone, download it again to check the read status
EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
return
@ -225,16 +202,6 @@ class MessageFragment : Fragment(), CoroutineScope {
showMessage()
}
private fun checkRecipients(): Boolean {
message.recipients?.forEach { recipient ->
if (recipient.id == -1L)
recipient.fullName = app.profile.accountName ?: app.profile.studentNameLong ?: ""
if (message.type == TYPE_SENT && recipient.readDate < 1)
return false
}
return true
}
private fun showMessage() {
b.body.text = MessagesUtils.htmlToSpannable(activity, message.body.toString())
b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM)
@ -245,6 +212,8 @@ class MessageFragment : Fragment(), CoroutineScope {
b.subject.text = message.subject
manager.setStarIcon(b.messageStar, message)
b.replyButton.isVisible = message.type == TYPE_RECEIVED || message.type == TYPE_DELETED
b.deleteButton.isVisible = message.type == TYPE_RECEIVED
if (message.type == TYPE_RECEIVED || message.type == TYPE_DELETED) {
@ -255,9 +224,9 @@ class MessageFragment : Fragment(), CoroutineScope {
fabIcon = CommunityMaterial.Icon3.cmd_reply_outline
}
setFabOnClickListener(View.OnClickListener {
setFabOnClickListener {
b.replyButton.performClick()
})
}
}
activity.gainAttentionFAB()
}

View File

@ -22,7 +22,8 @@ import kotlin.coroutines.CoroutineContext
class MessagesAdapter(
val activity: AppCompatActivity,
val teachers: List<Teacher>,
val onItemClick: ((item: MessageFull) -> Unit)? = null
val onItemClick: ((item: MessageFull) -> Unit)? = null,
val onStarClick: ((item: MessageFull) -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope, Filterable {
companion object {
private const val TAG = "MessagesAdapter"
@ -32,13 +33,17 @@ class MessagesAdapter(
private val app = activity.applicationContext as App
// optional: place the manager here
internal val manager
get() = app.messageManager
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// mutable var changed by the filter
var items = listOf<Any>()
var allItems = listOf<Any>()
// mutable list managed by the fragment
val allItems = mutableListOf<Any>()
val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) }
val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }

View File

@ -12,13 +12,11 @@ import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.databinding.MessagesListFragmentBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
@ -33,13 +31,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: MessagesListFragmentBinding
private var adapter: MessagesAdapter? = null
private lateinit var adapter: MessagesAdapter
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
private val manager
get() = app.messageManager
var teachers = listOf<Teacher>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -54,22 +54,28 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
var topPosition = arguments.getInt("topPosition", NO_POSITION)
var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION)
val searchText = arguments.getString("searchText", "")
val searchText = arguments?.getString("searchText")
teachers = withContext(Dispatchers.Default) {
app.db.teacherDao().getAllNow(App.profileId)
}
adapter = MessagesAdapter(activity, teachers) {
adapter = MessagesAdapter(activity, teachers, onItemClick = {
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
"messageId" to it.id
))
}, onStarClick = {
this@MessagesListFragment.launch {
manager.starMessage(it, !it.isStarred)
}
})
app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { messages ->
if (!isAdded) return@Observer
if (!isAdded || !this@MessagesListFragment::adapter.isInitialized)
return@Observer
messages.forEach { message ->
// uh oh, so these are the workarounds ??
message.recipients?.removeAll { it.profileId != message.profileId }
message.recipients?.forEach { recipient ->
if (recipient.fullName == null) {
@ -78,35 +84,46 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
}
}
// load & configure the adapter
val items = messages.toMutableList<Any>()
items.add(0, MessagesSearch().also {
it.searchText = searchText
})
adapter?.items = items
adapter?.allItems = items
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
if (searchText.isNotBlank())
adapter?.filter?.filter(searchText) {
b.list.adapter = adapter
// show/hide relevant views
setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT)
b.progressBar.isVisible = false
b.list.isVisible = messages.isNotEmpty()
b.noData.isVisible = messages.isEmpty()
if (messages.isEmpty()) {
return@Observer
}
else
b.list.adapter = adapter
if (adapter.allItems.isEmpty()) {
// items empty - add the search field
adapter.allItems += MessagesSearch().also {
it.searchText = searchText ?: ""
}
} else {
// items not empty - remove all messages
adapter.allItems.removeAll { it is MessageFull }
}
// add all messages
adapter.allItems.addAll(messages)
// configure the adapter & recycler view
if (b.list.adapter == null) {
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
if (messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT)
addOnScrollListener(onScrollListener)
this.adapter = this@MessagesListFragment.adapter
}
}
setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty())
val layoutManager = (b.list.layoutManager as? LinearLayoutManager) ?: return@Observer
(b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager ->
// reapply the filter
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
adapter.filter.filter(searchText ?: searchItem?.searchText, null)
// restore the previously saved scroll position
if (topPosition != NO_POSITION && topPosition > layoutManager.findLastCompletelyVisibleItemPosition()) {
b.list.scrollToPosition(topPosition)
} else if (bottomPosition != NO_POSITION && bottomPosition < layoutManager.findFirstVisibleItemPosition()) {
@ -114,26 +131,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
}
topPosition = NO_POSITION
bottomPosition = NO_POSITION
}
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
})
}; return true }
override fun onDestroy() {
super.onDestroy()
if (!isAdded)
if (!isAdded || !this::adapter.isInitialized)
return
val layoutManager = (b.list.layoutManager as? LinearLayoutManager)
val searchItem = adapter?.items?.firstOrNull { it is MessagesSearch } as? MessagesSearch
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
onPageDestroy?.invoke(position, Bundle(
"topPosition" to layoutManager?.findFirstVisibleItemPosition(),

View File

@ -13,11 +13,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AutoCompleteTextView
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hootsuite.nachos.chip.ChipInfo
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
@ -27,6 +25,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
@ -39,16 +38,12 @@ import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.span.BoldSpan
import pl.szczodrzynski.edziennik.utils.span.ItalicSpan
import pl.szczodrzynski.edziennik.utils.span.SubscriptSizeSpan
import pl.szczodrzynski.edziennik.utils.span.SuperscriptSizeSpan
import pl.szczodrzynski.edziennik.utils.span.*
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import kotlin.coroutines.CoroutineContext
import kotlin.text.replace
class MessagesComposeFragment : Fragment(), CoroutineScope {
companion object {
@ -63,14 +58,17 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// private val manager
// get() = app.messageManager
private val textStylingManager
get() = app.textStylingManager
private val profileConfig by lazy { app.config.forProfile().ui }
private val greetingText
get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
private var teachers = mutableListOf<Teacher>()
private var watchFormatChecked = true
private var watchSelectionChanged = true
private lateinit var stylingConfig: StylingConfig
private val enableTextStyling
get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN
@ -101,13 +99,6 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
// do your job
}
if (App.devMode) {
b.textHtml.isVisible = true
b.text.addTextChangedListener {
b.textHtml.text = getHtmlText()
}
}
activity.bottomSheet.prependItem(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_messages_config)
@ -126,48 +117,11 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
}
}
private fun getHtmlText(): String {
val text = b.text.text ?: return ""
// apparently setting the spans to a different Spannable calls the original EditText's
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
watchSelectionChanged = false
var textHtml = if (enableTextStyling) {
val spanned = SpannableString(text)
// remove zero-length spans, as they seem to affect
// the whole line when converted to HTML
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
spanned.removeSpan(it)
}
HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
.replace("</b><b>", "")
.replace("</i><i>", "")
.replace("</u><u>", "")
.replace("</sub><sub>", "")
.replace("</sup><sup>", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
} else {
text.toString()
}
watchSelectionChanged = true
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
textHtml = textHtml
.replace("<br>", "<p>&nbsp;</p>")
.replace("<b>", "<strong>")
.replace("</b>", "</strong>")
.replace("<i>", "<em>")
.replace("</i>", "</em>")
.replace("<u>", "<span style=\"text-decoration: underline;\">")
.replace("</u>", "</span>")
}
return textHtml
private fun getMessageBody(): String {
return if (enableTextStyling)
textStylingManager.getHtmlText(stylingConfig)
else
b.text.text?.toString() ?: ""
}
private fun getRecipientList() {
@ -185,80 +139,6 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
}
}
@Suppress("UNUSED_PARAMETER")
private fun onFormatChecked(
group: MaterialButtonToggleGroup,
checkedId: Int,
isChecked: Boolean
) {
if (!watchFormatChecked)
return
val span = when (checkedId) {
R.id.fontStyleBold -> BoldSpan()
R.id.fontStyleItalic -> ItalicSpan()
R.id.fontStyleUnderline -> UnderlineSpan()
R.id.fontStyleStrike -> StrikethroughSpan()
R.id.fontStyleSubscript -> SubscriptSizeSpan(10, dip = true)
R.id.fontStyleSuperscript -> SuperscriptSizeSpan(10, dip = true)
else -> return
}
// see comments in getHtmlText()
watchSelectionChanged = false
if (isChecked)
BetterHtml.applyFormat(span = span, editText = b.text)
else
BetterHtml.removeFormat(span = span, editText = b.text)
watchSelectionChanged = true
if (App.devMode)
b.textHtml.text = getHtmlText()
}
@Suppress("UNUSED_PARAMETER")
private fun onFormatClear(view: View) {
// shortened version on onFormatChecked(), removing all spans
watchSelectionChanged = false
BetterHtml.removeFormat(span = null, editText = b.text)
watchSelectionChanged = true
if (App.devMode)
b.textHtml.text = getHtmlText()
// force update of text style toggle states
onSelectionChanged(b.text.selectionStart, b.text.selectionEnd)
}
private fun onSelectionChanged(selectionStart: Int, selectionEnd: Int) {
if (!watchSelectionChanged)
return
val spanned = b.text.text ?: return
val spans = spanned.getSpans(selectionStart, selectionEnd, Any::class.java).mapNotNull {
if (it::class.java !in BetterHtml.customSpanClasses)
return@mapNotNull null
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
// remove 0-length spans after navigating out of them
if (spanStart == spanEnd)
spanned.removeSpan(it)
else if (spanned.getSpanFlags(it) hasSet SPAN_EXCLUSIVE_EXCLUSIVE)
spanned.setSpan(it, spanStart, spanEnd, SPAN_EXCLUSIVE_INCLUSIVE)
// names are helpful here
val isNotAfterWord = selectionEnd <= spanEnd
val isSelectionInWord = selectionStart != selectionEnd && selectionStart >= spanStart
val isCursorInWord = selectionStart == selectionEnd && selectionStart > spanStart
val isChecked = (isCursorInWord || isSelectionInWord) && isNotAfterWord
if (isChecked) it::class.java else null
}
watchFormatChecked = false
b.fontStyleBold.isChecked = BoldSpan::class.java in spans
b.fontStyleItalic.isChecked = ItalicSpan::class.java in spans
b.fontStyleUnderline.isChecked = UnderlineSpan::class.java in spans
b.fontStyleStrike.isChecked = StrikethroughSpan::class.java in spans
b.fontStyleSubscript.isChecked = SubscriptSizeSpan::class.java in spans
b.fontStyleSuperscript.isChecked = SuperscriptSizeSpan::class.java in spans
watchFormatChecked = true
}
private fun createView() {
b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
@ -318,54 +198,66 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
b.subjectLayout.isEnabled = false
b.textLayout.isEnabled = false
val styles = listOf(
StylingConfig.Style(
button = b.fontStyleBold,
spanClass = BoldSpan::class.java,
icon = CommunityMaterial.Icon2.cmd_format_bold,
hint = R.string.hint_style_bold,
),
StylingConfig.Style(
button = b.fontStyleItalic,
spanClass = ItalicSpan::class.java,
icon = CommunityMaterial.Icon2.cmd_format_italic,
hint = R.string.hint_style_italic,
),
StylingConfig.Style(
button = b.fontStyleUnderline,
// a custom span is used to prevent issues with keyboards which underline words
spanClass = UnderlineCustomSpan::class.java,
icon = CommunityMaterial.Icon2.cmd_format_underline,
hint = R.string.hint_style_underline,
),
StylingConfig.Style(
button = b.fontStyleStrike,
spanClass = StrikethroughSpan::class.java,
icon = CommunityMaterial.Icon2.cmd_format_strikethrough,
hint = R.string.hint_style_strike,
),
StylingConfig.Style(
button = b.fontStyleSubscript,
spanClass = SubscriptSizeSpan::class.java,
icon = CommunityMaterial.Icon2.cmd_format_subscript,
hint = R.string.hint_style_subscript,
),
StylingConfig.Style(
button = b.fontStyleSuperscript,
spanClass = SuperscriptSizeSpan::class.java,
icon = CommunityMaterial.Icon2.cmd_format_superscript,
hint = R.string.hint_style_superscript,
),
)
stylingConfig = StylingConfig(
editText = b.text,
fontStyleGroup = b.fontStyle,
fontStyleClear = b.fontStyleClear,
styles = styles,
textHtml = if (App.devMode) b.textHtml else null,
htmlCompatibleMode = app.profile.loginStoreType == LOGIN_TYPE_MOBIDZIENNIK,
)
b.fontStyleLayout.isVisible = enableTextStyling
b.fontStyleBold.isEnabled = false
b.fontStyleItalic.isEnabled = false
b.fontStyleUnderline.isEnabled = false
b.fontStyleStrike.isEnabled = false
b.fontStyleSubscript.isEnabled = false
b.fontStyleSuperscript.isEnabled = false
b.fontStyleClear.isEnabled = false
b.text.setOnFocusChangeListener { _, hasFocus ->
b.fontStyleBold.isEnabled = hasFocus
b.fontStyleItalic.isEnabled = hasFocus
b.fontStyleUnderline.isEnabled = hasFocus
b.fontStyleStrike.isEnabled = hasFocus
b.fontStyleSubscript.isEnabled = hasFocus
b.fontStyleSuperscript.isEnabled = hasFocus
b.fontStyleClear.isEnabled = hasFocus
if (enableTextStyling) {
textStylingManager.attach(stylingConfig)
}
b.fontStyleBold.text = CommunityMaterial.Icon2.cmd_format_bold.character.toString()
b.fontStyleItalic.text = CommunityMaterial.Icon2.cmd_format_italic.character.toString()
b.fontStyleUnderline.text = CommunityMaterial.Icon2.cmd_format_underline.character.toString()
b.fontStyleStrike.text = CommunityMaterial.Icon2.cmd_format_strikethrough.character.toString()
b.fontStyleSubscript.text = CommunityMaterial.Icon2.cmd_format_subscript.character.toString()
b.fontStyleSuperscript.text = CommunityMaterial.Icon2.cmd_format_superscript.character.toString()
b.fontStyleBold.attachToastHint(R.string.hint_style_bold)
b.fontStyleItalic.attachToastHint(R.string.hint_style_italic)
b.fontStyleUnderline.attachToastHint(R.string.hint_style_underline)
b.fontStyleStrike.attachToastHint(R.string.hint_style_strike)
b.fontStyleSubscript.attachToastHint(R.string.hint_style_subscript)
b.fontStyleSuperscript.attachToastHint(R.string.hint_style_superscript)
/*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
.toBuilder()
.setBottomLeftCornerSize(0f)
.build()
b.fontStyleSuperscript.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
.toBuilder()
.setBottomRightCornerSize(0f)
.build()
b.fontStyleClear.shapeAppearanceModel = b.fontStyleClear.shapeAppearanceModel
.toBuilder()
.setTopLeftCornerSize(0f)
.setTopRightCornerSize(0f)
.build()*/
b.fontStyle.addOnButtonCheckedListener(this::onFormatChecked)
b.fontStyleClear.setOnClickListener(this::onFormatClear)
b.text.setSelectionChangedListener(this::onSelectionChanged)
if (App.devMode) {
b.textHtml.isVisible = true
b.text.addTextChangedListener {
b.textHtml.text = getMessageBody()
}
}
activity.navView.bottomBar.apply {
fabEnable = true
@ -533,7 +425,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength)
return
val textHtml = getHtmlText()
val body = getMessageBody()
activity.bottomSheet.hideKeyboard()
@ -541,7 +433,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
.setTitle(R.string.messages_compose_confirm_title)
.setMessage(R.string.messages_compose_confirm_text)
.setPositiveButton(R.string.send) { _, _ ->
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml).enqueue(activity)
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), body).enqueue(activity)
}
.setNegativeButton(R.string.cancel, null)
.show()

View File

@ -13,15 +13,16 @@ class MessagesComparator : Comparator<Any> {
return 0
return when {
// standard sorting
// descending sorting (1. true, 2. false)
o1.isStarred && !o2.isStarred -> -1
!o1.isStarred && o2.isStarred -> 1
// ascending sorting
o1.filterWeight > o2.filterWeight -> 1
o1.filterWeight < o2.filterWeight -> -1
else -> when {
// reversed sorting
// descending sorting
o1.addedDate > o2.addedDate -> -1
o1.addedDate < o2.addedDate -> 1
else -> 0
}
}
}
}

View File

@ -55,6 +55,11 @@ class MessageViewHolder(
b.messageDate.setTextAppearance(activity, style)
b.messageDate.typeface = typeface
if (adapter.onStarClick == null) {
b.messageStar.isVisible = false
}
b.messageStar.detachToastHint()
val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12)
b.messageProfileBackground.setImageBitmap(messageInfo.profileImage)
b.messageSender.text = messageInfo.profileName
@ -75,5 +80,11 @@ class MessageViewHolder(
adapter.onItemClick?.let { listener ->
b.root.onClick { listener(item) }
}
adapter.onStarClick?.let { listener ->
b.messageStar.isVisible = true
adapter.manager.setStarIcon(b.messageStar, item)
b.messageStar.onClick { listener(item) }
b.messageStar.attachToastHint(R.string.hint_message_star)
}
}
}

View File

@ -26,7 +26,7 @@ object BetterHtml {
val customSpanClasses = listOf(
BoldSpan::class.java,
ItalicSpan::class.java,
UnderlineSpan::class.java,
UnderlineCustomSpan::class.java,
StrikethroughSpan::class.java,
SubscriptSizeSpan::class.java,
SuperscriptSizeSpan::class.java,
@ -101,8 +101,9 @@ object BetterHtml {
Typeface.ITALIC -> ItalicSpan()
else -> null
}
is SubscriptSpan -> SubscriptSizeSpan(size = 10, dip = true)
is SuperscriptSpan -> SuperscriptSizeSpan(size = 10, dip = true)
is UnderlineSpan -> UnderlineCustomSpan()
is SubscriptSpan -> SubscriptSizeSpan()
is SuperscriptSpan -> SuperscriptSizeSpan()
else -> null
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-7.
*/
package pl.szczodrzynski.edziennik.utils.managers
import android.os.Bundle
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorRes
import com.mikepenz.iconics.view.IconicsImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.navlib.colorAttr
class MessageManager(private val app: App) {
suspend fun getMessage(profileId: Int, args: Bundle?): MessageFull? {
val id = args?.getLong("messageId") ?: return null
val json = args.getString("message")
val addedDate = args.getLong("sentDate")
return getMessage(profileId, id, json, addedDate)
}
suspend fun getMessage(
profileId: Int,
id: Long,
json: String?,
sentDate: Long = 0L
): MessageFull? {
val message = if (json != null) {
app.gson.fromJson(json, MessageFull::class.java)?.also {
if (sentDate > 0L) {
it.addedDate = sentDate
}
}
} else {
withContext(Dispatchers.IO) {
app.db.messageDao().getByIdNow(profileId, id)
}
} ?: return null
// make recipients ID-unique
// this helps when multiple profiles receive the same message
// (there are multiple -1 recipients for the same message ID)
val recipientsDistinct = message.recipients?.distinctBy { it.id } ?: return null
message.recipients?.clear()
message.recipients?.addAll(recipientsDistinct)
// load recipients for sent messages
val teachers = withContext(Dispatchers.IO) {
app.db.teacherDao().getAllNow(profileId)
}
message.recipients?.forEach { recipient ->
// store the account name as a recipient
if (recipient.id == -1L)
recipient.fullName = app.profile.accountName ?: app.profile.studentNameLong
// lookup a teacher by the recipient ID
if (recipient.fullName == null)
recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: ""
// unset the readByEveryone flag
if (recipient.readDate < 1 && message.type == Message.TYPE_SENT)
message.readByEveryone = false
}
// store the account name as sender for sent messages
if (message.type == Message.TYPE_SENT && message.senderName == null) {
message.senderName = app.profile.accountName ?: app.profile.studentNameLong
}
// set the message as seen
if (message.body != null && !message.seen) {
app.db.metadataDao().setSeen(profileId, message, true)
}
//msg.recipients = app.db.messageRecipientDao().getAllByMessageId(msg.profileId, msg.id)
return message
}
fun setStarIcon(image: IconicsImageView, message: Message) {
if (message.isStarred) {
image.icon?.colorRes = R.color.md_amber_500
image.icon?.icon = CommunityMaterial.Icon3.cmd_star
} else {
image.icon?.colorAttr(image.context, android.R.attr.textColorSecondary)
image.icon?.icon = CommunityMaterial.Icon3.cmd_star_outline
}
}
suspend fun starMessage(message: Message, isStarred: Boolean) {
message.isStarred = isStarred
withContext(Dispatchers.Default) {
app.db.messageDao().replace(message)
}
}
suspend fun markAsDeleted(message: Message) {
message.type = Message.TYPE_DELETED
withContext(Dispatchers.Default) {
app.db.messageDao().replace(message)
}
}
}

View File

@ -0,0 +1,195 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-7.
*/
package pl.szczodrzynski.edziennik.utils.managers
import android.text.Spanned
import android.widget.Button
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.text.HtmlCompat
import com.google.android.material.button.MaterialButton
import com.google.android.material.button.MaterialButtonToggleGroup
import com.mikepenz.iconics.typeface.IIcon
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.attachToastHint
import pl.szczodrzynski.edziennik.hasSet
import pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
class TextStylingManager(private val app: App) {
companion object {
private const val TAG = "TextStylingManager"
}
data class StylingConfig(
val editText: TextInputKeyboardEdit,
val fontStyleGroup: MaterialButtonToggleGroup,
val fontStyleClear: Button,
val styles: List<Style>,
val textHtml: TextView? = null,
val htmlCompatibleMode: Boolean = false,
) {
data class Style(
val button: MaterialButton,
val spanClass: Class<*>,
val icon: IIcon,
@StringRes
val hint: Int,
) {
fun newInstance(): Any = spanClass.newInstance()
}
var watchStyleChecked = true
var watchSelectionChanged = true
}
fun attach(config: StylingConfig) {
enableButtons(config, enable = false)
config.editText.setOnFocusChangeListener { _, hasFocus ->
enableButtons(config, enable = hasFocus)
}
config.styles.forEach {
it.button.text = it.icon.character.toString()
it.button.attachToastHint(it.hint)
}
config.fontStyleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
val style = config.styles.firstOrNull {
it.button.id == checkedId
} ?: return@addOnButtonCheckedListener
onStyleChecked(config, style, isChecked)
}
config.fontStyleClear.setOnClickListener {
onStyleClear(config)
}
config.editText.setSelectionChangedListener { selectionStart, selectionEnd ->
onSelectionChanged(config, selectionStart, selectionEnd)
}
/*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
.toBuilder()
.setBottomLeftCornerSize(0f)
.build()
b.fontStyleSuperscript.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
.toBuilder()
.setBottomRightCornerSize(0f)
.build()
b.fontStyleClear.shapeAppearanceModel = b.fontStyleClear.shapeAppearanceModel
.toBuilder()
.setTopLeftCornerSize(0f)
.setTopRightCornerSize(0f)
.build()*/
}
fun getHtmlText(config: StylingConfig): String {
val spanned = config.editText.text ?: return ""
// apparently setting the spans to a different Spannable calls the original EditText's
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
config.watchSelectionChanged = false
// remove zero-length spans, as they seem to affect
// the whole line when converted to HTML
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
spanned.removeSpan(it)
}
var textHtml = HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
.replace("</b><b>", "")
.replace("</i><i>", "")
.replace("</u><u>", "")
.replace("</sub><sub>", "")
.replace("</sup><sup>", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
config.watchSelectionChanged = true
if (config.htmlCompatibleMode) {
textHtml = textHtml
.replace("<br>", "<p>&nbsp;</p>")
.replace("<b>", "<strong>")
.replace("</b>", "</strong>")
.replace("<i>", "<em>")
.replace("</i>", "</em>")
.replace("<u>", "<span style=\"text-decoration: underline;\">")
.replace("</u>", "</span>")
}
return textHtml
}
private fun onStyleChecked(
config: StylingConfig,
style: StylingConfig.Style,
isChecked: Boolean
) {
if (!config.watchStyleChecked)
return
val span = style.newInstance()
// see comments in getHtmlText()
config.watchSelectionChanged = false
if (isChecked)
BetterHtml.applyFormat(span, config.editText)
else
BetterHtml.removeFormat(span, config.editText)
config.watchSelectionChanged = true
config.textHtml?.text = getHtmlText(config)
}
private fun onStyleClear(config: StylingConfig) {
// shortened version on onStyleChecked(), removing all spans
config.watchSelectionChanged = false
BetterHtml.removeFormat(span = null, config.editText)
config.watchSelectionChanged = true
config.textHtml?.text = getHtmlText(config)
// force update of text style toggle states
onSelectionChanged(config, config.editText.selectionStart, config.editText.selectionEnd)
}
private fun onSelectionChanged(config: StylingConfig, selectionStart: Int, selectionEnd: Int) {
if (!config.watchSelectionChanged)
return
val spanned = config.editText.text ?: return
val spans = spanned.getSpans(selectionStart, selectionEnd, Any::class.java).mapNotNull {
if (it::class.java !in BetterHtml.customSpanClasses)
return@mapNotNull null
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
// remove 0-length spans after navigating out of them
if (spanStart == spanEnd)
spanned.removeSpan(it)
else if (spanned.getSpanFlags(it) hasSet Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
spanned.setSpan(it, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
// names are helpful here
val isNotAfterWord = selectionEnd <= spanEnd
val isSelectionInWord = selectionStart != selectionEnd && selectionStart >= spanStart
val isCursorInWord = selectionStart == selectionEnd && selectionStart > spanStart
val isChecked = (isCursorInWord || isSelectionInWord) && isNotAfterWord
if (isChecked) it::class.java else null
}
config.watchStyleChecked = false
config.styles.forEach {
it.button.isChecked = it.spanClass in spans
}
config.watchStyleChecked = true
}
private fun enableButtons(config: StylingConfig, enable: Boolean) {
config.fontStyleClear.isEnabled = enable
config.styles.forEach {
it.button.isEnabled = enable
}
}
}

View File

@ -7,10 +7,10 @@ package pl.szczodrzynski.edziennik.utils.span
import android.text.TextPaint
import android.text.style.SubscriptSpan
class SubscriptSizeSpan(
private val size: Int,
private val dip: Boolean,
) : SubscriptSpan() {
class SubscriptSizeSpan : SubscriptSpan() {
private val size = 10
private val dip = true
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)

View File

@ -7,10 +7,10 @@ package pl.szczodrzynski.edziennik.utils.span
import android.text.TextPaint
import android.text.style.SuperscriptSpan
class SuperscriptSizeSpan(
private val size: Int,
private val dip: Boolean,
) : SuperscriptSpan() {
class SuperscriptSizeSpan : SuperscriptSpan() {
private val size = 10
private val dip = true
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-7.
*/
package pl.szczodrzynski.edziennik.utils.span
import android.text.style.UnderlineSpan
class UnderlineCustomSpan : UnderlineSpan()

View File

@ -25,6 +25,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/attendances_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -153,6 +153,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/attendances_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -30,6 +30,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -25,6 +25,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/homework_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -2,21 +2,22 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-12.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorSurface_6dp"
android:minHeight="?attr/actionBarSize"
android:orientation="horizontal"
android:background="@color/colorSurface_6dp">
android:orientation="horizontal">
<ImageButton
android:id="@+id/closeButton"
@ -55,7 +56,7 @@
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="visible"
tools:visibility="gone"/>
tools:visibility="gone" />
<androidx.core.widget.NestedScrollView
android:id="@+id/content"
@ -81,6 +82,7 @@
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/profileBackground"
android:layout_width="64dp"
@ -112,26 +114,38 @@
<TextView
android:id="@+id/sender"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackground"
android:ellipsize="end"
android:maxLines="2"
android:minHeight="64dp"
android:paddingHorizontal="8dp"
android:paddingTop="12dp"
android:paddingVertical="8dp"
android:textAppearance="@style/NavView.TextView.Subtitle"
tools:text="Allegro - wysyłamy duużo wiadomości!!! Masz nowe oferty! Możesz kupić nowego laptopa! Ale super! Ehh, to jest nadawca a nie temat więc nwm czemu to tutaj wpisałem" />
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/messageStar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:background="?selectableItemBackgroundBorderless"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-star-outline"
app:iiv_size="24dp"
tools:background="@android:drawable/star_off" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="8dp"
android:gravity="center_horizontal"
android:textAppearance="@style/NavView.TextView.Small"
tools:text="14:26" />
tools:text="20 lis 2021\n14:26" />
</LinearLayout>
<TextView
@ -188,122 +202,61 @@
android:visibility="visible"
tools:visibility="visible">
<LinearLayout
<com.google.android.material.button.MaterialButton
android:id="@+id/replyButton"
android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="4dp"
android:paddingVertical="8dp">
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/replyIcon"
android:layout_width="24dp"
android:layout_height="24dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-reply-outline"
tools:srcCompat="@android:drawable/ic_menu_revert" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Odpowiedz"
android:textAppearance="@style/NavView.TextView.Small" />
</LinearLayout>
<LinearLayout
android:id="@+id/forwardButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="4dp"
android:paddingVertical="8dp">
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/forwardIcon"
android:layout_width="24dp"
android:layout_height="24dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-arrow-right"
tools:srcCompat="@android:drawable/ic_media_ff" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Przekaż dalej"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/message_reply"
android:textAllCaps="false"
android:textAppearance="@style/NavView.TextView.Small" />
tools:drawableTop="@android:drawable/sym_action_email" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/forwardButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:paddingHorizontal="4dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/message_forward"
android:textAllCaps="false"
tools:drawableTop="@android:drawable/stat_sys_phone_call_forward" />
<LinearLayout
<com.google.android.material.button.MaterialButton
android:id="@+id/deleteButton"
android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="4dp"
android:paddingVertical="8dp"
android:visibility="visible">
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/message_delete"
android:textAllCaps="false"
tools:drawableTop="@android:drawable/ic_menu_delete" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-delete-outline"
tools:srcCompat="@android:drawable/ic_menu_delete" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Usuń"
android:textAppearance="@style/NavView.TextView.Small" />
</LinearLayout>
<LinearLayout
<com.google.android.material.button.MaterialButton
android:id="@+id/downloadButton"
android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="4dp"
android:paddingVertical="8dp"
android:visibility="visible">
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-download-outline"
tools:srcCompat="@android:drawable/ic_menu_delete" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Pobierz ponownie"
android:textAppearance="@style/NavView.TextView.Small" />
</LinearLayout>
android:paddingTop="16dp"
android:text="@string/message_download"
android:textAllCaps="false"
android:visibility="gone"
tools:drawableTop="@android:drawable/stat_sys_download" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -25,6 +25,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/messages_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -2,10 +2,9 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-4-4.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -40,9 +39,11 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:paddingEnd="32dp"
android:paddingRight="32dp"
android:singleLine="true"
android:textStyle="normal"
android:textAppearance="@style/NavView.TextView.Helper"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/messageProfileBackground"
app:layout_constraintTop_toBottomOf="@+id/messageSender"
@ -72,6 +73,8 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginBottom="12dp"
android:paddingEnd="32dp"
android:paddingRight="32dp"
android:singleLine="true"
android:textAppearance="@style/NavView.TextView.Helper"
app:layout_constraintBottom_toBottomOf="parent"
@ -80,6 +83,23 @@
app:layout_constraintTop_toBottomOf="@+id/messageSubject"
tools:text="Znajdź produkty, których szukasz. Witaj Kuba Szczodrzyński (Client" />
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/messageStar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-star-outline"
app:iiv_size="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageDate"
tools:background="@android:drawable/star_off" />
<TextView
android:id="@+id/messageDate"
android:layout_width="wrap_content"
@ -106,6 +126,6 @@
app:layout_constraintBottom_toBottomOf="@+id/messageDate"
app:layout_constraintEnd_toStartOf="@+id/messageDate"
app:layout_constraintTop_toTopOf="@+id/messageDate"
tools:srcCompat="@tools:sample/avatars[4]" />
tools:background="@tools:sample/avatars[4]" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -25,6 +25,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/notifications_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -30,6 +30,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -25,6 +25,7 @@
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -744,7 +744,7 @@
<string name="messages_compose_text_hint">Napisz wiadomość…</string>
<string name="messages_compose_title">Napisz wiadomość</string>
<string name="messages_compose_to_hint">Do</string>
<string name="messages_date_time_format" translatable="false">%s, %s</string>
<string name="messages_date_time_format" translatable="false">%s\n%s</string>
<string name="messages_delete_confirmation">Czy chcesz usunąć wiadomość?</string>
<string name="messages_delete_confirmation_text">Spowoduje to przeniesienie wiadomości do zakładki \"Usunięte\" w aplikacji. Zmiany nie wpłyną na wiadomość w e-dzienniku (nie zostanie ona tam usunięta).</string>
<string name="messages_download_error">Błąd pobierania wiadomości</string>
@ -1471,4 +1471,9 @@
<string name="hint_style_subscript">Indeks dolny</string>
<string name="hint_style_superscript">Indeks górny</string>
<string name="messages_compose_style_clear">Wyczyść format</string>
<string name="hint_message_star">Oznacz gwiazdką</string>
<string name="message_forward">Przekaż dalej</string>
<string name="message_delete">Usuń</string>
<string name="message_reply">Odpowiedz</string>
<string name="message_download">Pobierz ponownie</string>
</resources>