Skip to content

Commit

Permalink
feat(back): types messages yarn lint!
Browse files Browse the repository at this point in the history
  • Loading branch information
agjini committed Oct 11, 2024
1 parent ab67f18 commit eeec1b4
Show file tree
Hide file tree
Showing 20 changed files with 386 additions and 238 deletions.
4 changes: 2 additions & 2 deletions app/src/components/trip/TripSurveyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ActivityIndicator, StyleSheet, View } from "react-native";
import { AppText } from "@/components/base/AppText.tsx";
import { AppColors } from "@/theme/colors.ts";
import { AppLocalization } from "@/api/i18n.ts";
import { addSeconds, capitalize, CoLiane, DayOfWeekFlag, Liane, LianeMessage, RallyingPoint, TimeOnlyUtils, TripMessage } from "@liane/common";
import { addSeconds, capitalize, CoLiane, DayOfWeekFlag, Liane, LianeMessage, RallyingPoint, TimeOnlyUtils, TripAdded } from "@liane/common";
import React, { useCallback, useContext, useMemo, useState } from "react";
import { Center, Column, Row } from "@/components/base/AppLayout.tsx";
import { UserPicture } from "@/components/UserPicture.tsx";
Expand All @@ -17,7 +17,7 @@ import { SimpleModal } from "@/components/modal/SimpleModal.tsx";
import { DayOfTheWeekPicker } from "@/components/DayOfTheWeekPicker.tsx";
import { TimeWheelPicker } from "@/components/TimeWheelPicker.tsx";

export const TripSurveyView = ({ message, coLiane, color }: { message: LianeMessage<TripMessage>; coLiane: CoLiane; color: string }) => {
export const TripSurveyView = ({ message, coLiane, color }: { message: LianeMessage<TripAdded>; coLiane: CoLiane; color: string }) => {
const { services, user } = useContext(AppContext);

const trip = useQuery(LianeDetailQueryKey(message.content.value), () => services.liane.get(message.content.value));
Expand Down
13 changes: 1 addition & 12 deletions app/src/screens/communities/CommunitiesChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import { AppStorage } from "@/api/storage.ts";
import { TimeWheelPicker } from "@/components/TimeWheelPicker.tsx";
import { DayOfTheWeekPicker } from "@/components/DayOfTheWeekPicker.tsx";
import { MessageBubble } from "@/screens/communities/MessageBubble.tsx";
import { AppRoundedButton } from "@/components/base/AppRoundedButton.tsx";

export const CommunitiesChatScreen = () => {
const { navigation, route } = useAppNavigation<"CommunitiesChat">();
Expand Down Expand Up @@ -115,7 +114,7 @@ export const CommunitiesChatScreen = () => {
setTripModalVisible(false);
const geolocationLevel = await AppStorage.getSetting("geolocation");

const created = await services.liane.post({
await services.liane.post({
liane: liane!.id!,
arriveAt: time[0].toISOString(),
returnAt: time[1]?.toISOString(),
Expand All @@ -125,16 +124,6 @@ export const CommunitiesChatScreen = () => {
geolocationLevel: geolocationLevel || "None",
recurrence: undefined
});
await services.community.sendMessage(liane!.id!, {
type: "Trip",
value: created.id!
});
if (created.return) {
await services.community.sendMessage(liane!.id!, {
type: "Trip",
value: created.return
});
}
};

useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions app/src/screens/communities/MessageBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { capitalize, CoLiane, LianeMessage, Ref, TripMessage, User } from "@liane/common";
import { capitalize, CoLiane, LianeMessage, Ref, TripAdded, User } from "@liane/common";
import React from "react";
import { View } from "react-native";
import { AppColorPalettes, AppColors } from "@/theme/colors";
Expand Down Expand Up @@ -63,7 +63,7 @@ export const MessageBubble = ({
<AppText style={{ fontSize: 12, alignSelf: isSender ? "flex-end" : "flex-start", color }}>{date}</AppText>
</>
) : (
<TripSurveyView message={message as LianeMessage<TripMessage>} coLiane={coLiane} color={color} />
<TripSurveyView message={message as LianeMessage<TripAdded>} coLiane={coLiane} color={color} />
)}
</Column>
</Column>
Expand Down
7 changes: 1 addition & 6 deletions back/src/Liane/Liane.Api/Auth/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,7 @@ public static string GetPseudo(string? firstName, string? lastName)
return "Utilisateur inconnu";
}

if (lastName is null || lastName.Length == 0)
{
return firstName;
}

return $"{firstName} {lastName[0]}.";
return firstName;
}

public static FullUser Unknown(string userId) => new(
Expand Down
16 changes: 16 additions & 0 deletions back/src/Liane/Liane.Api/Community/ILianeMessageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Liane.Api.Util.Pagination;
using Liane.Api.Util.Ref;

namespace Liane.Api.Community;

public interface ILianeMessageService
{
Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Liane> liane) => GetMessages(liane, Pagination.Empty);
Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Liane> liane, Pagination pagination);
Task<LianeMessage> SendMessage(Ref<Liane> liane, MessageContent content);
Task MarkAsRead(Ref<Liane> liane, DateTime timestamp);
Task<ImmutableDictionary<Ref<Liane>, int>> GetUnreadLianes();
}
9 changes: 0 additions & 9 deletions back/src/Liane/Liane.Api/Community/ILianeService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Liane.Api.Util.Pagination;
using Liane.Api.Util.Ref;

namespace Liane.Api.Community;
Expand All @@ -20,11 +18,4 @@ public interface ILianeService
Task<bool> Leave(Ref<Liane> liane);

Task<bool> JoinTrip(JoinTripQuery query);

Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Liane> liane) => GetMessages(liane, Pagination.Empty);
Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Liane> liane, Pagination pagination);
Task<LianeMessage> SendMessage(Ref<Liane> liane, MessageContent content);

Task MarkAsRead(Ref<Liane> liane, DateTime timestamp);
Task<ImmutableDictionary<Ref<Liane>, int>> GetUnreadLianes();
}
3 changes: 2 additions & 1 deletion back/src/Liane/Liane.Api/Community/Liane.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace Liane.Api.Community;
public sealed record Liane(
Guid Id,
ImmutableList<LianeMember> Members,
ImmutableList<LianeMember> PendingMembers
ImmutableList<LianeMember> PendingMembers,
Ref<User> CreatedBy
) : IIdentity<Guid>, ISharedResource<LianeMember>
{
public bool IsMember(Ref<User> user) => Members.Any(m => m.User.Id == user.Id) || PendingMembers.Any(m => m.User.Id == user.Id);
Expand Down
28 changes: 19 additions & 9 deletions back/src/Liane/Liane.Api/Community/LianeMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,27 @@ private MessageContent()
{
}

public string AsText() =>
this switch
{
Text text => text.Value,
Trip => "Nouveau trajet",
_ => throw new ArgumentOutOfRangeException()
};
public abstract string Value { get; init; }

public static implicit operator MessageContent(string value) => new Text(value);

public sealed record Text(string Value) : MessageContent;

public sealed record Trip(Ref<ApiTrip> Value) : MessageContent;
}
public sealed record LianeRequestModified(string Value, Ref<LianeRequest> LianeRequest) : MessageContent;

public sealed record MemberRequested(string Value, Ref<User> User, Ref<LianeRequest> LianeRequest) : MessageContent;

public sealed record MemberAdded(string Value, Ref<User> User, Ref<LianeRequest> LianeRequest) : MessageContent;

public sealed record MemberRejected(string Value, Ref<User> User) : MessageContent;

public sealed record MemberLeft(string Value, Ref<User> User) : MessageContent;

public sealed record TripAdded(string Value, Ref<ApiTrip> Trip) : MessageContent;

public sealed record TripRemoved(string Value, Ref<ApiTrip> Trip) : MessageContent;

public sealed record MemberJoinedTrip(string Value, Ref<User> User, Ref<ApiTrip> Trip, bool TakeReturn) : MessageContent;

public sealed record MemberLeftTrip(string Value, Ref<User> User, Ref<ApiTrip> Trip) : MessageContent;
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public sealed class LianeFetcher(LianeRequestFetcher lianeRequestFetcher, IUserS
return new Api.Community.Liane(
lianeRequestId,
lianeMembers.ToImmutableList(),
lianePendingMembers.ToImmutableList()
lianePendingMembers.ToImmutableList(),
lianeRequest.CreatedBy!
);
}))
.ToImmutableDictionary(l => l.Id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using System;
using System.Collections.Immutable;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Liane.Api.Auth;
using Liane.Api.Community;
using Liane.Api.Util.Pagination;
using Liane.Api.Util.Ref;
using Liane.Service.Internal.Event;
using Liane.Service.Internal.Postgis.Db;
using Liane.Service.Internal.Util;
using Liane.Service.Internal.Util.Sql;
using Microsoft.IdentityModel.Tokens;
using UuidExtensions;

namespace Liane.Service.Internal.Community;

public sealed class LianeMessageServiceImpl(
PostgisDatabase db,
ICurrentContext currentContext,
LianeFetcher lianeFetcher,
IPushService pushService,
IUserService userService
) : ILianeMessageService
{
private static readonly TimeZoneInfo TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris");
private static readonly CultureInfo Culture = new("fr-FR");

public async Task<LianeMessage> SendMessage(Ref<Api.Community.Liane> liane, MessageContent content)
{
using var connection = db.NewConnection();
using var tx = connection.BeginTransaction();
var userId = currentContext.CurrentUser().Id;
var lianeId = Guid.Parse(liane.Id);
var resolvedLiane = await lianeFetcher.FetchLiane(connection, lianeId, tx);
if (!resolvedLiane.IsMember(userId))
{
throw new UnauthorizedAccessException("User is not part of the liane");
}

var id = Uuid7.Guid();
var now = DateTime.UtcNow;

content = content with { Value = await FormatMessage(userId, content) };

await connection.MergeAsync(new LianeMessageDb(id, lianeId, content, userId, now), tx);
var lianeMessage = new LianeMessage(id.ToString(), userId, now, content);

await pushService.PushMessage(lianeId, lianeMessage);
tx.Commit();
return lianeMessage;
}

private async Task<string> FormatMessage(Ref<Api.Auth.User> sender, MessageContent content)
{
var value = content.Value.Trim();
if (!value.IsNullOrEmpty())
{
return value;
}

var sentBy = await FormatUser(await sender.Resolve(userService.Get));
return content switch
{
MessageContent.LianeRequestModified => $"{sentBy} a modifié son annonce.",
MessageContent.TripAdded m => $"{sentBy} lance un covoit pour le {FormatDate(m.Trip.Value?.DepartureTime)}",
MessageContent.TripRemoved m => $"{sentBy} a supprimé son covoit pour le {FormatDate(m.Trip.Value?.DepartureTime)}",
MessageContent.MemberRequested => $"{sentBy} souhaite rejoindre la liane.",
MessageContent.MemberAdded m => $"{sentBy} a accepté {await FormatUser(m.User)} dans la liane.",
MessageContent.MemberRejected m => $"{sentBy} a rejeté la demande de {await FormatUser(m.User)} pour rejoindre la liane.",
MessageContent.MemberLeft m => $"{await FormatUser(m.User)} a quitté la liane",
MessageContent.MemberJoinedTrip m => await FormatJoinedTrip(m),
MessageContent.MemberLeftTrip m => $"{await FormatUser(m.User)} a quitté le trajet du {FormatDate(m.Trip.Value?.DepartureTime)}",
_ => throw new ArgumentOutOfRangeException(nameof(content))
};
}

private async Task<string> FormatJoinedTrip(MessageContent.MemberJoinedTrip m)
{
var msg = $"{await FormatUser(m.User)} a rejoint le trajet du {FormatDate(m.Trip.Value?.DepartureTime)}";
if (m.TakeReturn)
{
return msg + " (retour inclus)";
}

return msg;
}

private static string FormatDate(DateTime? dateTime) =>
dateTime is null
? "???"
: TimeZoneInfo.ConvertTime(dateTime.Value, TimeZone).ToString("dddd d MMMM", Culture);

private async Task<string> FormatUser(Ref<Api.Auth.User>? user)
{
if (user is null)
{
return "???";
}

var resolved = await user.Resolve(userService.Get);

return resolved.Pseudo;
}

public async Task<PaginatedResponse<LianeMessage>> GetMessages(Ref<Api.Community.Liane> liane, Pagination pagination)
{
using var connection = db.NewConnection();
using var tx = connection.BeginTransaction();

var lianeId = liane.IdAsGuid();
var member = await MarkAsRead(connection, lianeId, tx, DateTime.UtcNow);

var filter = Filter<LianeMessageDb>.Where(m => m.LianeId, ComparisonOperator.Eq, lianeId)
& Filter<LianeMessageDb>.Where(m => m.CreatedAt, ComparisonOperator.Gt, member.JoinedAt);

var query = Query.Select<LianeMessageDb>()
.Where(filter)
.And(pagination.ToFilter<LianeMessageDb>())
.OrderBy(m => m.Id, false)
.OrderBy(m => m.CreatedAt, false)
.Take(pagination.Limit + 1);

var total = await connection.QuerySingleAsync<int>(Query.Count<LianeMessageDb>().Where(filter), tx);
var result = await connection.QueryAsync(query, tx);

tx.Commit();

var hasNext = result.Count > pagination.Limit;
var cursor = hasNext ? result.Last().ToCursor() : null;
return new PaginatedResponse<LianeMessage>(
Math.Min(result.Count, pagination.Limit),
cursor,
result.Take(pagination.Limit)
.Select(m => new LianeMessage(m.Id.ToString(), m.CreatedBy, m.CreatedAt, m.Content))
.ToImmutableList(),
total);
}

public async Task<ImmutableDictionary<Ref<Api.Community.Liane>, int>> GetUnreadLianes()
{
using var connection = db.NewConnection();
var userId = currentContext.CurrentUser().Id;
var unread = await connection.QueryAsync<(Guid, int)>("""
SELECT m.liane_id, COUNT(msg.id) AS unread
FROM liane_member m
INNER JOIN liane_request r ON m.liane_request_id = r.id
LEFT JOIN liane_message msg ON msg.liane_id = m.liane_id AND msg.created_at > m.joined_at
WHERE m.joined_at IS NOT NULL AND r.created_by = @userId
AND (m.last_read_at IS NULL OR msg.created_at > m.last_read_at)
GROUP BY m.liane_id
""",
new { userId }
);
return unread.ToImmutableDictionary(m => (Ref<Api.Community.Liane>)m.Item1.ToString(), m => m.Item2);
}

public async Task MarkAsRead(Ref<Api.Community.Liane> liane, DateTime timestamp)
{
using var connection = db.NewConnection();
using var tx = connection.BeginTransaction();
var lianeId = Guid.Parse(liane.Id);
await MarkAsRead(connection, lianeId, tx, timestamp);
tx.Commit();
}

public async Task<LianeMemberDb?> TryGetMember(IDbConnection connection, Guid lianeId, string? userId, IDbTransaction? tx)
{
var userIdValue = userId ?? currentContext.CurrentUser().Id;
var lianeMemberDb = await connection.QueryFirstOrDefaultAsync<LianeMemberDb>("""
SELECT liane_member.liane_request_id, liane_member.liane_id, liane_member.requested_at, liane_member.joined_at, liane_member.last_read_at
FROM liane_member
INNER JOIN liane_request ON liane_member.liane_request_id = liane_request.id
WHERE liane_member.liane_id = @lianeId AND liane_request.created_by = @userId
""", new { userId = userIdValue, lianeId }, tx);
return lianeMemberDb;
}

private async Task<LianeMemberDb> MarkAsRead(IDbConnection connection, Guid lianeId, IDbTransaction tx, DateTime now)
{
var lianeMemberDb = await CheckIsMember(connection, lianeId, tx: tx);

var update = Query.Update<LianeMemberDb>()
.Set(m => m.LastReadAt, now)
.Where(l => l.LianeRequestId, ComparisonOperator.Eq, lianeMemberDb.LianeRequestId)
.And(l => l.LianeId, ComparisonOperator.Eq, lianeMemberDb.LianeId);
await connection.UpdateAsync(update, tx);

return lianeMemberDb with { LastReadAt = now };
}

private async Task<LianeMemberDb> CheckIsMember(IDbConnection connection, Guid lianeId, string? userId = null, IDbTransaction? tx = null)
{
var lianeMemberDb = await TryGetMember(connection, lianeId, userId, tx);

if (lianeMemberDb is null)
{
throw new UnauthorizedAccessException("User is not part of the liane");
}

return lianeMemberDb;
}
}
Loading

0 comments on commit eeec1b4

Please sign in to comment.