新增聊天消息提醒

This commit is contained in:
2024-10-11 16:51:51 +08:00
parent 36739b1615
commit 81f90db1b1
15 changed files with 414 additions and 105 deletions

View File

@@ -37,9 +37,9 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:label="@string/app_name"
android:theme="@style/Theme.RiderPro">
android:theme="@style/Theme.RiderPro"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -69,6 +69,9 @@
<action android:name="cn.jiguang.user.service.action" />
</intent-filter>
</service>
<service
android:name=".TrtcService"
android:exported="false" />
<receiver
android:name=".JpushReciver"

View File

@@ -1,6 +1,7 @@
package com.aiosman.riderpro
import android.content.Context
import android.content.Intent
import android.icu.util.Calendar
import android.icu.util.TimeZone
import android.util.Log
@@ -23,7 +24,6 @@ import com.tencent.imsdk.v2.V2TIMLogListener
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMSDKConfig
import com.tencent.imsdk.v2.V2TIMUserFullInfo
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.suspendCoroutine
@@ -66,8 +66,12 @@ object AppState {
val sign = accountService.getMyTrtcSign()
loginToTrtc(sign.userId, sign.sig)
updateTrtcUserProfile()
// 登录成功后启动TrtcService
context.startService(
Intent(context, TrtcService::class.java)
)
} catch (e: Exception) {
e.printStackTrace()
}
}
@@ -105,7 +109,7 @@ object AppState {
}
}
fun ReloadAppState() {
fun ReloadAppState(context: Context) {
// 重置动态列表页面
MomentViewModel.ResetModel()
// 重置我的页面
@@ -129,5 +133,13 @@ object AppState {
// 重置关注通知页面
IndexViewModel.ResetModel()
UserId = null
// 关闭 TrtcService
val trtcService = Intent(
context,
TrtcService::class.java
)
context.stopService(trtcService)
}
}

View File

@@ -0,0 +1,40 @@
package com.aiosman.riderpro
import com.aiosman.riderpro.data.ChatService
import com.aiosman.riderpro.data.ChatServiceImpl
import com.aiosman.riderpro.entity.ChatNotification
/**
* 保存一些关于聊天的状态
*/
object ChatState {
val chatService: ChatService = ChatServiceImpl()
var chatNotificationList = mutableListOf<ChatNotification>()
suspend fun getStrategyByTargetTrtcId(targetTrtcId: String): ChatNotification? {
// 先从缓存中查找
if (chatNotificationList.isNotEmpty()) {
chatNotificationList.forEach {
if (it.targetTrtcId == targetTrtcId) {
return it
}
}
}
// 缓存中没有再从网络获取
chatService.getChatNotifications(targetTrtcId)?.let {
chatNotificationList.add(it)
return it
}
// 存在未设置策略的情况
return null
}
suspend fun updateChatNotification(targetUserId: Int, strategy: String): ChatNotification {
val updatedData = chatService.updateChatNotification(targetUserId, strategy)
chatNotificationList = chatNotificationList.filter {
it.targetUserId != targetUserId
}.toMutableList().apply {
add(updatedData)
}
return updatedData
}
}

View File

@@ -26,6 +26,8 @@ import cn.jiguang.api.utils.JCollectionAuth
import cn.jpush.android.api.JPushInterface
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.ui.Navigation
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.navigateToPost
@@ -109,7 +111,7 @@ class MainActivity : ComponentActivity() {
// 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID")
var action = intent.getStringExtra("ACTION")
val action = intent.getStringExtra("ACTION")
if (action == "newFollow") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
@@ -118,6 +120,25 @@ class MainActivity : ComponentActivity() {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "TRTC_NEW_MESSAGE") {
val userService:UserService = UserServiceImpl()
val sender = intent.getStringExtra("SENDER")
sender?.let {
scope.launch {
try {
val profile = userService.getUserProfileByTrtcUserId(it)
navController.navigate(NavigationRoute.Chat.route.replace(
"{id}",
profile.id.toString()
))
}catch (e:Exception){
e.printStackTrace()
}
}
}
return@Navigation
}
if (commentId == null) {
commentId = "0"
}

View File

@@ -0,0 +1,129 @@
package com.aiosman.riderpro
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.aiosman.riderpro.entity.ChatItem
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TrtcService : Service() {
private var trtcMessageListener: V2TIMAdvancedMsgListener? = null
private val channelId = "chat_notification"
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("TrtcService", "onStartCommand")
createNotificationChannel()
CoroutineScope(Dispatchers.IO).launch {
registerMessageListener(applicationContext)
}
return START_STICKY;
}
override fun onDestroy() {
super.onDestroy()
Log.d("TrtcService", "onDestroy")
CoroutineScope(Dispatchers.IO).launch {
unRegisterMessageListener()
}
}
private fun registerMessageListener(context: Context) {
val scope = CoroutineScope(Dispatchers.IO)
trtcMessageListener = object : V2TIMAdvancedMsgListener() {
override fun onRecvNewMessage(msg: V2TIMMessage?) {
super.onRecvNewMessage(msg)
msg?.let {
if (MainActivityLifecycle.isForeground) {
return
}
scope.launch {
// 先获取通知策略
val notiStrategy = ChatState.getStrategyByTargetTrtcId(it.sender)
if (notiStrategy == null) {
// 未设置策略, 默认通知
sendNotification(context, it)
return@launch
}
if (notiStrategy.strategy != "mute") {
sendNotification(context, it)
}
}
}
}
}
V2TIMManager.getMessageManager().addAdvancedMsgListener(trtcMessageListener)
}
private fun unRegisterMessageListener() {
V2TIMManager.getMessageManager().removeAdvancedMsgListener(trtcMessageListener)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "Chat Notification"
val descriptionText = "Notification for chat message"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(channelId, name, importance).apply {
description = descriptionText
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun sendNotification(context: Context, message: V2TIMMessage) {
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("ACTION", "TRTC_NEW_MESSAGE")
putExtra("SENDER", message.sender)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val chatItem = ChatItem.convertToChatItem(message, context) ?: return
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.mipmap.rider_pro_log_round)
.setContentTitle(chatItem.nickname)
.setContentText(chatItem.textDisplay)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notify(message.msgID.hashCode(), builder.build())
}
}
}

View File

@@ -0,0 +1,42 @@
package com.aiosman.riderpro.data
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.api.UpdateChatNotificationRequestBody
import com.aiosman.riderpro.entity.ChatNotification
interface ChatService {
suspend fun getChatNotifications(
targetTrtcId: String
): ChatNotification?
suspend fun updateChatNotification(
targetUserId: Int,
strategy: String
): ChatNotification
}
class ChatServiceImpl : ChatService {
override suspend fun getChatNotifications(
targetTrtcId: String
): ChatNotification? {
val resp = ApiClient.api.getChatNotification(targetTrtcId)
if (resp.isSuccessful) {
return resp.body()?.data
}
return null
}
override suspend fun updateChatNotification(
targetUserId: Int,
strategy: String
): ChatNotification {
val resp = ApiClient.api.updateChatNotification(UpdateChatNotificationRequestBody(
targetUserId = targetUserId,
strategy = strategy
))
if (resp.isSuccessful) {
return resp.body()?.data!!
}
throw Exception("update chat notification failed")
}
}

View File

@@ -9,6 +9,7 @@ import com.aiosman.riderpro.data.Comment
import com.aiosman.riderpro.data.DataContainer
import com.aiosman.riderpro.data.ListContainer
import com.aiosman.riderpro.data.Moment
import com.aiosman.riderpro.entity.ChatNotification
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -154,31 +155,6 @@ data class GenerateLoginCaptchaRequestBody(
@SerializedName("username")
val username: String,
)
//{
// "id":48,
// "dot": [
// {
// "index": 0,
// "x": 76,
// "y": 165
// },
// {
// "index": 1,
// "x": 144,
// "y": 21
// },
// {
// "index": 2,
// "x": 220,
// "y": 42
// },
// {
// "index": 3,
// "x": 10,
// "y": 10
// }
// ]
//}
data class DotPosition(
@SerializedName("index")
val index: Int,
@@ -193,6 +169,15 @@ data class CaptchaInfo(
@SerializedName("dot")
val dot: List<DotPosition>
)
data class UpdateChatNotificationRequestBody(
@SerializedName("targetUserId")
val targetUserId: Int,
@SerializedName("strategy")
val strategy: String,
)
interface RiderProAPI {
@POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@@ -417,4 +402,14 @@ interface RiderProAPI {
@Body body: GenerateLoginCaptchaRequestBody
): Response<DataContainer<CaptchaResponseBody>>
}
@GET("chat/notification")
suspend fun getChatNotification(
@Query("targetTrtcId") targetTrtcId: String
): Response<DataContainer<ChatNotification>>
@POST("chat/notification")
suspend fun updateChatNotification(
@Body body: UpdateChatNotificationRequestBody
): Response<DataContainer<ChatNotification>>
}

View File

@@ -3,6 +3,7 @@ package com.aiosman.riderpro.entity
import android.content.Context
import android.icu.util.Calendar
import com.aiosman.riderpro.exp.formatChatTime
import com.google.gson.annotations.SerializedName
import com.tencent.imsdk.v2.V2TIMImageElem
import com.tencent.imsdk.v2.V2TIMMessage
@@ -15,11 +16,11 @@ data class ChatItem(
val timeCategory: String = "",
val timestamp: Long = 0,
val imageList: MutableList<V2TIMImageElem.V2TIMImage> = emptyList<V2TIMImageElem.V2TIMImage>().toMutableList(),
val messageType : Int = 0,
val textDisplay : String = "",
val messageType: Int = 0,
val textDisplay: String = "",
val msgId: String, // Add this property
var showTimestamp: Boolean = false,
var showTimeDivider:Boolean = false
var showTimeDivider: Boolean = false
) {
companion object {
fun convertToChatItem(message: V2TIMMessage, context: Context): ChatItem? {
@@ -36,7 +37,7 @@ data class ChatItem(
val timestamp = message.timestamp
val calendar = Calendar.getInstance()
calendar.timeInMillis = timestamp * 1000
val imageElm = message.imageElem?.imageList
val imageElm = message.imageElem?.imageList
when (message.elemType) {
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
val imageElm = message.imageElem?.imageList?.all {
@@ -74,8 +75,8 @@ data class ChatItem(
textDisplay = message.textElem?.text ?: "Unsupported message type",
msgId = message.msgID // Add this line to include msgId
)
}
else -> {
return null
}
@@ -85,3 +86,16 @@ data class ChatItem(
}
}
data class ChatNotification(
@SerializedName("userId")
val userId: Int,
@SerializedName("userTrtcId")
val userTrtcId: String,
@SerializedName("targetUserId")
val targetUserId: Int,
@SerializedName("targetTrtcId")
val targetTrtcId: String,
@SerializedName("strategy")
val strategy: String
)

View File

@@ -66,12 +66,15 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.ChatItem
import com.aiosman.riderpro.exp.formatChatTime
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DropdownMenu
import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.tencent.imsdk.v2.V2TIMMessage
@@ -80,6 +83,7 @@ import kotlinx.coroutines.launch
@Composable
fun ChatScreen(userId: String) {
var isMenuExpanded by remember { mutableStateOf(false) }
val navController = LocalNavController.current
val context = LocalNavController.current.context
val viewModel = viewModel<ChatViewModel>(
@@ -131,7 +135,7 @@ fun ChatScreen(userId: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
.padding(vertical = 16.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
@@ -154,6 +158,41 @@ fun ChatScreen(userId: String) {
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
)
Spacer(modifier = Modifier.weight(1f))
Box {
Image(
painter = painterResource(R.drawable.rider_pro_more_horizon),
modifier = Modifier
.size(28.dp)
.noRippleClickable {
isMenuExpanded = true
},
contentDescription = null
)
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
menuItems = listOf(
MenuItem(
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
){
isMenuExpanded = false
viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active")
} else {
viewModel.updateNotificationStrategy("mute")
}
}
}
),
)
}
}
}
},

View File

@@ -1,7 +1,6 @@
package com.aiosman.riderpro.ui.chat
import android.content.Context
import android.icu.util.Calendar
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
@@ -11,16 +10,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.riderpro.ChatState
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.entity.ChatItem
import com.aiosman.riderpro.exp.formatChatTime
import com.aiosman.riderpro.entity.ChatNotification
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
import com.tencent.imsdk.v2.V2TIMCallback
import com.tencent.imsdk.v2.V2TIMImageElem
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMMessage
import com.tencent.imsdk.v2.V2TIMSendCallback
@@ -44,6 +43,7 @@ class ChatViewModel(
var isLoading by mutableStateOf(false)
var lastMessage: V2TIMMessage? = null
val showTimestampMap = mutableMapOf<String, Boolean>() // Add this map
var chatNotification by mutableStateOf<ChatNotification?>(null)
fun init(context: Context) {
// 获取用户信息
viewModelScope.launch {
@@ -53,6 +53,9 @@ class ChatViewModel(
RegistListener(context)
fetchHistoryMessage(context)
// 获取通知信息
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
chatNotification = notiStrategy
}
}
@@ -243,4 +246,13 @@ class ChatViewModel(
}
return list
}
suspend fun updateNotificationStrategy(strategy: String) {
userProfile?.let {
val result = ChatState.updateChatNotification(it.id, strategy)
chatNotification = result
}
}
val notificationStrategy get() = chatNotification?.strategy ?: "default"
}

View File

@@ -73,13 +73,13 @@ object MyProfileViewModel : ViewModel() {
}
}
suspend fun logout() {
suspend fun logout(context: Context) {
AppStore.apply {
token = null
rememberMe = false
saveData()
}
AppState.ReloadAppState()
AppState.ReloadAppState(context)
}
fun updateUserProfileBanner(bannerImageUrl: Uri?,file:File, context: Context) {

View File

@@ -378,62 +378,65 @@ fun ProfileV3(
color = Color.Black
)
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
) {
if (isSelf) {
Box(
modifier = Modifier
.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
modifier = Modifier.noRippleClickable {
minibarExpanded = true
},
tint = Color.Black
)
}
DropdownMenu(
expanded = minibarExpanded,
onDismissRequest = { minibarExpanded = false },
width = 250,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
minibarExpanded = false
scope.launch {
onLogout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
Box(
modifier = Modifier
.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
modifier = Modifier.noRippleClickable {
minibarExpanded = true
},
tint = Color.Black
)
}
DropdownMenu(
expanded = minibarExpanded,
onDismissRequest = { minibarExpanded = false },
width = 250,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
minibarExpanded = false
scope.launch {
onLogout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
}
}
},
MenuItem(
stringResource(R.string.change_password),
R.mipmap.rider_pro_change_password
) {
minibarExpanded = false
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
},
MenuItem(
stringResource(R.string.favourites),
R.drawable.rider_pro_favourite
) {
minibarExpanded = false
scope.launch {
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
},
MenuItem(
stringResource(R.string.change_password),
R.mipmap.rider_pro_change_password
) {
minibarExpanded = false
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
},
MenuItem(
stringResource(R.string.favourites),
R.drawable.rider_pro_favourite
) {
minibarExpanded = false
scope.launch {
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
)
)
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}

View File

@@ -2,6 +2,7 @@ package com.aiosman.riderpro.ui.index.tabs.profile
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
@@ -9,29 +10,17 @@ import kotlinx.coroutines.launch
fun ProfileWrap(
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
MyProfileViewModel.loadProfile()
}
// ProfileV2(
// onUpdateBanner = { uri, context ->
// MyProfileViewModel.updateUserProfileBanner(uri, context)
// },
// onLogout = {
// MyProfileViewModel.viewModelScope.launch {
// MyProfileViewModel.logout()
// }
//
// },
// profile = MyProfileViewModel.profile,
// sharedFlow = MyProfileViewModel.sharedFlow
// )
ProfileV3(
onUpdateBanner = { uri, file, context ->
MyProfileViewModel.updateUserProfileBanner(uri, file, context)
},
onLogout = {
MyProfileViewModel.viewModelScope.launch {
MyProfileViewModel.logout()
MyProfileViewModel.logout(context)
}
},

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:alpha="0.77" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.63,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.64,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2zM16,17L8,17v-6c0,-2.48 1.51,-4.5 4,-4.5s4,2.02 4,4.5v6z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:alpha="0.77" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM12,6.5c2.49,0 4,2.02 4,4.5v0.1l2,2L18,11c0,-3.07 -1.63,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68c-0.24,0.06 -0.47,0.15 -0.69,0.23l1.64,1.64c0.18,-0.02 0.36,-0.05 0.55,-0.05zM5.41,3.35L4,4.76l2.81,2.81C6.29,8.57 6,9.74 6,11v5l-2,2v1h14.24l1.74,1.74 1.41,-1.41L5.41,3.35zM16,17L8,17v-6c0,-0.68 0.12,-1.32 0.34,-1.9L16,16.76L16,17z"/>
</vector>