auth = $auth; $this->log = $log; $this->redirect = $redirect; $this->response = $response; $this->request = $request; $this->db = $db; $this->message = $message; $this->user = $user; } public function index(): Response { return $this->listConversations(); } /** * Returns a list of conversations of the current user, each containing the other participant, * the most recent message, and the number of unread messages. */ public function listConversations(): Response { $current_user = $this->auth->user(); $latest_messages = $this->latestMessagePerConversation($current_user); $numberOfUnreadMessages = $this->numberOfUnreadMessagesPerConversation($current_user); $conversations = []; foreach ($latest_messages as $msg) { $other_user = $msg->user_id == $current_user->id ? $msg->receiver : $msg->sender; $unread_messages = $numberOfUnreadMessages[$other_user->id] ?? 0; array_push($conversations, [ 'other_user' => $other_user, 'latest_message' => $msg, 'unread_messages' => $unread_messages ]); } $users = $this->user->orderBy('name')->get() ->except($current_user->id) ->mapWithKeys(function ($u) { return [ $u->id => $u->nameWithPronoun() ]; }); return $this->response->withView( 'pages/messages/overview.twig', [ 'conversations' => $conversations, 'users' => $users ] ); } /** * Forwards to the conversation with the user of the given id. */ public function toConversation(Request $request): Response { $data = $this->validate($request, [ 'user_id' => 'required|int' ]); return $this->redirect->to('/messages/' . $data['user_id']); } /** * Returns a list of messages between the current user and a user with the given id. The ids shall not be the same. * Unread messages will be marked as read during this call. Still, they will be shown as unread in the frontend to * highlight them to the user as new. */ public function conversation(Request $request): Response { $current_user = $this->auth->user(); $other_user = $this->user->findOrFail($request->getAttribute('user_id')); if ($current_user->id == $other_user->id) { throw new HttpForbidden('You can not start a conversation with yourself.'); } $messages = $this->message ->where(function ($q) use ($current_user, $other_user) { $q->whereUserId($current_user->id) ->whereReceiverId($other_user->id); }) ->orWhere(function ($q) use ($current_user, $other_user) { $q->whereUserId($other_user->id) ->whereReceiverId($current_user->id); }) ->orderBy('created_at') ->get(); $unread_messages = $messages->filter(function ($m) use ($other_user) { return $m->user_id == $other_user->id && !$m->read; }); foreach ($unread_messages as $msg) { $msg->read = true; $msg->save(); $msg->read = false; // change back to true to display it to the frontend one more time. } return $this->response->withView( 'pages/messages/conversation.twig', ['messages' => $messages, 'other_user' => $other_user] ); } /** * Sends a message to another user. */ public function send(Request $request): Response { $current_user = $this->auth->user(); $data = $this->validate($request, [ 'text' => 'required' ]); $other_user = $this->user->findOrFail($request->getAttribute('user_id')); if ($other_user->id == $current_user->id) { throw new HttpForbidden('You can not send a message to yourself.'); } $new_message = new Message(); $new_message->sender()->associate($current_user); $new_message->receiver()->associate($other_user); $new_message->text = $data['text']; $new_message->read = false; $new_message->save(); $this->log->info( 'User {from} has written a message to user {to}', [ 'from' => $current_user->id, 'to' => $other_user->id ] ); return $this->redirect->to('/messages/' . $other_user->id); } /** * Deletes a message from a given id, as long as this message was send by the current user. The given user_id * The given user_id is used to redirect back to the conversation with that user. */ public function delete(Request $request): Response { $current_user = $this->auth->user(); $other_user_id = $request->getAttribute('user_id'); $msg_id = $request->getAttribute('msg_id'); $msg = $this->message->findOrFail($msg_id); if ($msg->user_id == $current_user->id) { $msg->delete(); $this->log->info( 'User {from} deleted message {msg} in a conversation with user {to}', [ 'from' => $current_user->id, 'to' => $other_user_id, 'msg' => $msg_id ] ); } else { $this->log->warning( 'User {from} tried to delete message {msg} which was not written by them, ' . 'in a conversation with user {to}', [ 'from' => $current_user->id, 'to' => $other_user_id, 'msg' => $msg_id ] ); throw new HttpForbidden('You can not delete a message you haven\'t send'); } return $this->redirect->to('/messages/' . $other_user_id); } public function numberOfUnreadMessages(): int { return $this->auth->user() ->messagesReceived() ->where('read', false) ->count(); } protected function numberOfUnreadMessagesPerConversation($current_user): Collection { return $current_user->messagesReceived() ->select('user_id', $this->raw('count(*) as amount')) ->where('read', false) ->groupBy('user_id') ->get(['user_id', 'amount']) ->mapWithKeys(function ($unread) { return [ $unread->user_id => $unread->amount ]; }); } protected function latestMessagePerConversation($current_user): Collection { $latest_message_ids = $this->message ->select($this->raw('max(id) as last_id')) ->where('user_id', "=", $current_user->id) ->orWhere('receiver_id', "=", $current_user->id) ->groupBy($this->raw( '(CASE WHEN user_id = ' . $current_user->id . ' THEN receiver_id ELSE user_id END)' )); return $this->message ->joinSub($latest_message_ids, 'conversations', function ($join) { $join->on('messages.id', '=', 'conversations.last_id'); }) ->orderBy('created_at', 'DESC') ->get(); } protected function raw($value): QueryExpression { return $this->db->getConnection()->raw($value); } }