Skip to content

Commit 55a1995

Browse files
feat: Status notifications (#2117)
* feat: Status notifications The status (availability) affects the notifications now. If the status is set to 'Away' the notifications are not displayed at all. If it's 'Busy' the notifications are displayed only if certain requirements are met. Calls also are affected in a similar way.
1 parent f79360b commit 55a1995

File tree

8 files changed

+242
-99
lines changed

8 files changed

+242
-99
lines changed

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ ext {
2222
playServicesVersion = '15.0.1'
2323
audioVersion = System.getenv("AUDIO_VERSION") ?: '1.209.0@aar'
2424
stethoVersion = '1.5.0'
25-
zMessagingVersion = "141.0.2269"
25+
zMessagingVersion = "141.0.2271"
2626
paging_version = "1.0.0"
2727

2828
avsVersion = '4.9.170@aar'

app/src/main/res/values/strings.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,21 @@
10691069
<string name="availability_text_busy">%1$s IS BUSY</string>
10701070
<string name="availability_setstatus">SET A STATUS</string>
10711071

1072+
<string name="availability_notification_warning_away_title">You are set to Away</string>
1073+
<string name="availability_notification_warning_away">You will appear as Away to other people. You will not receive notifications about any incoming calls or messages.</string>
1074+
<string name="availability_notification_warning_busy_title">You are set to Busy</string>
1075+
<string name="availability_notification_warning_busy">You will appear as Busy to other people. You will only receive notifications for mentions, replies, and calls in conversations that are not muted.</string>
1076+
<string name="availability_notification_warning_available_title">You are set to Available</string>
1077+
<string name="availability_notification_warning_available">You will appear as Available to other people. You will receive notifications for incoming calls and for messages according to the Notifications setting in each conversation.</string>
1078+
<string name="availability_notification_warning_nostatus_title">No status set</string>
1079+
<string name="availability_notification_warning_nostatus">You will receive notifications for incoming calls and for messages according to the Notifications setting in each conversation.</string>
1080+
<string name="availability_notification_dont_show">Do not display this information again.</string>
1081+
<string name="availability_notification_ok">Ok</string>
1082+
<string name="availability_notification_blocked_title">Notifications are disabled in %1$s</string>
1083+
<string name="availability_notification_blocked">Status affects notifications now. You\'re set to \"Away\" and won\'t receive any notifications.</string>
1084+
<string name="availability_notification_changed_title">Notifications have changed in %1$s</string>
1085+
<string name="availability_notification_changed">Status affects notifications now. You\'re set to \"Busy\" and will only receive notifications when someone mentions you or replies to one of your messages.</string>
1086+
10721087
<string name="preferences_profile_new">New Team or Account</string>
10731088
<string name="preferences_profile_add_account">Add an account</string>
10741089
<string name="preferences_profile_create_team">Create a team</string>

app/src/main/scala/com/waz/services/calling/CallingNotificationsService.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ import android.content.{Context, Intent}
2222
import android.os.IBinder
2323
import com.waz.zclient.ServiceHelper
2424
import com.waz.zclient.notifications.controllers.CallingNotificationsController
25-
import com.waz.zclient.notifications.controllers.CallingNotificationsController.androidNotificationBuilder
25+
import com.waz.zclient.notifications.controllers.CallingNotificationsController.{NotificationAction, androidNotificationBuilder}
2626

2727
class CallingNotificationsService extends ServiceHelper {
2828
private lazy val callNCtrl = inject[CallingNotificationsController]
2929

3030
implicit lazy val cxt: Context = getApplicationContext
3131

3232
private lazy val sub = callNCtrl.notifications.map(_.find(_.isMainCall)).onUi {
33-
case Some(not) =>
33+
case Some(not) if not.action != NotificationAction.Nothing =>
3434
val builder = androidNotificationBuilder(not)
3535
startForeground(not.convId.str.hashCode, builder.build())
3636
case _ =>

app/src/main/scala/com/waz/zclient/MainActivity.scala

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ import android.graphics.drawable.ColorDrawable
2424
import android.graphics.{Color, Paint, PixelFormat}
2525
import android.os.{Build, Bundle}
2626
import android.support.v4.app.{Fragment, FragmentTransaction}
27+
import com.waz.content.{TeamsStorage, UserPreferences}
2728
import com.waz.content.UserPreferences._
2829
import com.waz.log.BasicLogging.LogTag.DerivedLogTag
2930
import com.waz.model.UserData.ConnectionStatus.{apply => _}
30-
import com.waz.model.{ConvId, UserId}
31+
import com.waz.model._
3132
import com.waz.service.AccountManager.ClientRegistrationState.{LimitReached, PasswordMissing, Registered, Unregistered}
3233
import com.waz.service.ZMessaging.clock
3334
import com.waz.service.{AccountManager, AccountsService, ZMessaging}
@@ -48,15 +49,17 @@ import com.waz.zclient.deeplinks.DeepLinkService.Error.{InvalidToken, SSOLoginTo
4849
import com.waz.zclient.deeplinks.DeepLinkService._
4950
import com.waz.zclient.fragments.ConnectivityFragment
5051
import com.waz.zclient.log.LogUI._
52+
import com.waz.zclient.messages.UsersController
5153
import com.waz.zclient.messages.controllers.NavigationController
54+
import com.waz.zclient.notifications.controllers.MessageNotificationsController
5255
import com.waz.zclient.pages.main.MainPhoneFragment
5356
import com.waz.zclient.pages.startup.UpdateFragment
5457
import com.waz.zclient.preferences.PreferencesActivity
5558
import com.waz.zclient.preferences.dialogs.ChangeHandleFragment
5659
import com.waz.zclient.tracking.UiTrackingController
5760
import com.waz.zclient.utils.ContextUtils._
5861
import com.waz.zclient.utils.StringUtils.TextDrawing
59-
import com.waz.zclient.utils.{Emojis, IntentUtils, ViewUtils}
62+
import com.waz.zclient.utils.{Emojis, IntentUtils, ResString, ViewUtils}
6063
import com.waz.zclient.views.LoadingIndicatorView
6164

6265
import scala.collection.JavaConverters._
@@ -86,6 +89,8 @@ class MainActivity extends BaseActivity
8689
private lazy val spinnerController = inject[SpinnerController]
8790
private lazy val passwordController = inject[PasswordController]
8891
private lazy val deepLinkService = inject[DeepLinkService]
92+
private lazy val usersController = inject[UsersController]
93+
private lazy val userPreferences = inject[Signal[UserPreferences]]
8994

9095
override def onAttachedToWindow(): Unit = {
9196
super.onAttachedToWindow()
@@ -137,6 +142,32 @@ class MainActivity extends BaseActivity
137142
case false =>
138143
}
139144

145+
(for {
146+
Some(user) <- userAccountsController.currentUser
147+
teamName <- user.teamId.fold(Signal.const(Option.empty[Name]))(teamId => Signal.future(inject[TeamsStorage].get(teamId).map(_.map(_.name))))
148+
prefs <- userPreferences
149+
shouldWarn <- prefs(UserPreferences.ShouldWarnStatusNotifications).signal
150+
avVisible <- usersController.availabilityVisible
151+
} yield (shouldWarn && avVisible, user.availability, teamName)).onUi {
152+
case (true, Availability.Away, Some(teamName)) =>
153+
inject[MessageNotificationsController].showAppNotification(
154+
ResString(R.string.availability_notification_blocked_title, teamName.str),
155+
ResString(R.string.availability_notification_blocked)
156+
)
157+
userPreferences.head.foreach(prefs =>
158+
prefs(UserPreferences.ShouldWarnStatusNotifications) := false
159+
)
160+
case (true, Availability.Busy, Some(teamName)) =>
161+
inject[MessageNotificationsController].showAppNotification(
162+
ResString(R.string.availability_notification_changed_title, teamName.str),
163+
ResString(R.string.availability_notification_changed)
164+
)
165+
userPreferences.head.foreach(prefs =>
166+
prefs(UserPreferences.ShouldWarnStatusNotifications) := false
167+
)
168+
case _ =>
169+
}
170+
140171
ForceUpdateActivity.checkBlacklist(this)
141172

142173
val loadingIndicator = findViewById[LoadingIndicatorView](R.id.progress_spinner)

app/src/main/scala/com/waz/zclient/notifications/controllers/CallingNotificationsController.scala

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ import android.os.Build
2525
import android.support.v4.app.NotificationCompat
2626
import com.waz.bitmap.BitmapUtils
2727
import com.waz.content.UserPreferences
28-
import com.waz.model.{ConvId, LocalInstant, Name, UserId}
28+
import com.waz.model._
2929
import com.waz.service.assets.AssetService.BitmapResult.BitmapLoaded
30-
import com.waz.service.call.CallInfo
3130
import com.waz.service.call.CallInfo.CallState._
3231
import com.waz.service.{AccountManager, AccountsService, GlobalModule, ZMessaging}
3332
import com.waz.services.calling.CallWakeService._
@@ -65,49 +64,66 @@ class CallingNotificationsController(implicit cxt: WireContext, eventContext: Ev
6564
val filteredGlobalProfile: Signal[(Option[ConvId], Seq[(ConvId, (UserId, UserId))])] =
6665
for {
6766
globalProfile <- inject[GlobalModule].calling.globalCallProfile
68-
curCallId = globalProfile.activeCall.map(_.convId)
69-
allCalls = globalProfile.calls.values.filter(c => c.state == OtherCalling || (curCallId.contains(c.convId) && c.state != Ongoing))
70-
.map(c => c.convId -> (c.caller, c.account)).toSeq
67+
curCallId = globalProfile.activeCall.map(_.convId)
68+
allCalls = globalProfile
69+
.calls
70+
.values
71+
.filter(c => c.state == OtherCalling || (curCallId.contains(c.convId) && c.state != Ongoing))
72+
.map(c => c.convId -> (c.caller, c.account))
73+
.toSeq
7174
} yield (curCallId, allCalls)
7275

7376
val notifications =
7477
for {
75-
zs <- inject[AccountsService].zmsInstances
78+
zs <- inject[AccountsService].zmsInstances
7679
(curCallId, allCallsF) <- filteredGlobalProfile
77-
bitmaps <- Signal.sequence(allCallsF.map { case (conv, (caller, account)) =>
78-
zs.find(_.selfUserId == account).fold2(Signal.const(conv -> Option.empty[Bitmap]), z => getBitmapSignal(z, caller).map(conv -> _))
79-
}: _*).map(_.toMap)
80-
notInfo <- Signal.sequence(allCallsF.map { case (conv, (caller, account)) =>
81-
zs.find(_.selfUserId == account).fold2(Signal.const(Option.empty[CallInfo], Name.Empty, Name.Empty, false),
82-
z => Signal(z.calling.joinableCallsNotMuted.map(_.get(conv)),
83-
z.usersStorage.optSignal(caller).map(_.map(_.name).getOrElse(Name.Empty)),
84-
z.convsStorage.optSignal(conv).map(_.map(_.displayName).getOrElse(Name.Empty)),
85-
z.conversations.groupConversation(conv))).map(conv -> _)
86-
}: _*)
87-
notificationData = notInfo.collect {
88-
case (convId, (Some(callInfo), title, msg, isGroup)) =>
89-
val action = callInfo.state match {
90-
case OtherCalling => NotificationAction.DeclineOrJoin
91-
case SelfConnected | SelfCalling | SelfJoining => NotificationAction.Leave
92-
case _ => NotificationAction.Nothing
93-
}
94-
CallNotification(
95-
convId.str.hashCode,
96-
convId,
97-
callInfo.account,
98-
callInfo.startTime,
99-
title,
100-
msg,
101-
bitmaps.getOrElse(convId, None),
102-
curCallId.contains(convId),
103-
action,
104-
callInfo.isVideoCall,
105-
isGroup)
106-
}
80+
bitmaps <- Signal.sequence(allCallsF.map { case (conv, (caller, account)) =>
81+
zs.find(_.selfUserId == account)
82+
.fold2(
83+
Signal.const(conv -> Option.empty[Bitmap]),
84+
z => getBitmapSignal(z, caller).map(conv -> _)
85+
)
86+
}: _*).map(_.toMap)
87+
notInfo <- Signal.sequence(allCallsF.map { case (conv, (caller, account)) =>
88+
zs.find(_.selfUserId == account)
89+
.fold2(
90+
Signal.const(None, Name.Empty, None, false, Availability.None),
91+
z => Signal(
92+
z.calling.joinableCallsNotMuted.map(_.get(conv)),
93+
z.usersStorage.optSignal(caller).map(_.map(_.name).getOrElse(Name.Empty)),
94+
z.convsStorage.optSignal(conv),
95+
z.conversations.groupConversation(conv),
96+
z.usersStorage.optSignal(z.selfUserId).map(_.fold[Availability](Availability.None)(_.availability))
97+
)).map(conv -> _)
98+
}: _*)
99+
notificationData = notInfo.collect {
100+
case (convId, (Some(callInfo), title, conv, isGroup, availability)) =>
101+
val muteSet = conv.fold(MuteSet.AllMuted)(_.muted)
102+
val action = (availability, muteSet, callInfo.state) match {
103+
case (Availability.Away, _, _) => NotificationAction.Nothing
104+
case (Availability.Busy, MuteSet.AllMuted, _) => NotificationAction.Nothing
105+
case (_, _, OtherCalling) => NotificationAction.DeclineOrJoin
106+
case (_, _, SelfConnected | SelfCalling | SelfJoining) => NotificationAction.Leave
107+
case _ => NotificationAction.Nothing
108+
}
109+
CallNotification(
110+
convId.str.hashCode,
111+
convId,
112+
callInfo.account,
113+
callInfo.startTime,
114+
title,
115+
conv.fold(Name.Empty)(_.displayName),
116+
bitmaps.getOrElse(convId, None),
117+
curCallId.contains(convId),
118+
action,
119+
callInfo.isVideoCall,
120+
isGroup
121+
)
122+
}
107123
} yield notificationData.sortWith {
108124
case (cn1, _) if curCallId.contains(cn1.convId) => false
109125
case (_, cn2) if curCallId.contains(cn2.convId) => true
110-
case (cn1, cn2) => cn1.convId.str > cn2.convId.str
126+
case (cn1, cn2) => cn1.convId.str > cn2.convId.str
111127
}
112128

113129
private lazy val currentNotificationsPref = inject[Signal[AccountManager]].map(_.userPrefs(UserPreferences.CurrentNotifications))
@@ -139,27 +155,27 @@ class CallingNotificationsController(implicit cxt: WireContext, eventContext: Ev
139155
verbose(l"${nots.size} call notifications")
140156

141157
cancelNots(nots)
142-
nots.foreach { not =>
143-
144-
val builder = androidNotificationBuilder(not)
145-
146-
def showNotification() = {
147-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
148-
verbose(l"Adding not: ${not.id}")
149-
currentNotificationsPref.head.foreach(_.mutate(_ + not.id))
158+
nots.foreach {
159+
case not if not.action != NotificationAction.Nothing =>
160+
val builder = androidNotificationBuilder(not)
161+
162+
def showNotification() = {
163+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
164+
verbose(l"Adding not: ${not.id}")
165+
currentNotificationsPref.head.foreach(_.mutate(_ + not.id))
166+
}
167+
notificationManager.notify(CallNotificationTag, not.id, builder.build())
150168
}
151-
notificationManager.notify(CallNotificationTag, not.id, builder.build())
152-
}
153169

154-
Try(showNotification()).recover {
155-
case NonFatal(e) =>
156-
error(l"Notify failed: try without bitmap", e)
157-
builder.setLargeIcon(null)
158-
try showNotification()
159-
catch {
160-
case NonFatal(e2) => error(l"second display attempt failed, aborting", e2)
161-
}
162-
}
170+
Try(showNotification()).recover {
171+
case NonFatal(e) =>
172+
error(l"Notify failed: try without bitmap", e)
173+
builder.setLargeIcon(null)
174+
Try(showNotification()).recover {
175+
case NonFatal(e2) => error(l"second display attempt failed, aborting", e2)
176+
}
177+
}
178+
case _ =>
163179
}
164180
}
165181

app/src/main/scala/com/waz/zclient/notifications/controllers/MessageNotificationsController.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import com.waz.zclient.messages.controllers.NavigationController
4848
import com.waz.zclient.utils.ContextUtils.{getInt, getIntArray, toPx}
4949
import com.waz.zclient.utils.{ResString, RingtoneUtils}
5050
import com.waz.zclient.{BuildConfig, Injectable, Injector, R}
51+
import org.threeten.bp.Instant
5152

5253
import scala.concurrent.Future
5354
import scala.concurrent.duration._
@@ -107,6 +108,35 @@ class MessageNotificationsController(bundleEnabled: Boolean = Build.VERSION.SDK_
107108
} yield {}
108109
}
109110

111+
def showAppNotification(title: ResString, body: ResString): Future[Unit] = {
112+
val contentTitle = SpannableWrapper(title, List(Span(Span.StyleSpanBold, Span.HeaderRange)))
113+
val contentText = SpannableWrapper(
114+
header = ResString(""),
115+
body = body,
116+
spans = List(Span(Span.ForegroundColorSpanBlack, Span.HeaderRange)),
117+
separator = ""
118+
)
119+
for {
120+
accountId <- selfId.head
121+
color <- notificationColor(accountId)
122+
} yield {
123+
val props = NotificationProps(
124+
accountId,
125+
when = Some(Instant.now().toEpochMilli),
126+
showWhen = Some(true),
127+
category = Some(NotificationCompat.CATEGORY_MESSAGE),
128+
priority = Some(NotificationCompat.PRIORITY_HIGH),
129+
smallIcon = Some(R.drawable.ic_menu_logo),
130+
openAccountIntent = Some(accountId),
131+
color = color,
132+
contentTitle = Some(contentTitle),
133+
contentText = Some(contentText),
134+
style = Some(StyleBuilder(StyleBuilder.BigText, title = contentTitle, bigText = Some(contentText)))
135+
)
136+
notificationManager.showNotification(accountId.hashCode(), props)
137+
}
138+
}
139+
110140
private def fetchTeamName(userId: UserId) =
111141
for {
112142
storage <- userStorage.head

app/src/main/scala/com/waz/zclient/notifications/controllers/NotificationManagerWrapper.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ case class NotificationProps(accountId: UserId,
187187
"priority" -> priority,
188188
"smallIcon" -> smallIcon,
189189
"contentTitle" -> contentTitle,
190-
"contentText " -> contentText,
190+
"contentText" -> contentText,
191191
"style" -> style,
192192
"groupSummary" -> groupSummary,
193193
"openAccountIntent" -> openAccountIntent,

0 commit comments

Comments
 (0)