Skip to content

Commit 7b5d890

Browse files
rafaelmf3Rafael Marinho
andauthored
[CHA-652]: feat(cha-769): support shared locations (#172)
* feat(cha-769): support shared locations * feat(cha-652): add location sharing support * fix(cha-652): fix model * fix(cha-652): fix unit tests * fix(cha-652): fix unit tests * fix(cha-652): fix unit tests * fix(cha-652): fix unit test * lint(cha-652): lint * fix warnings * add lint to the makefile * fix lint * fix lint * refactor * clean up --------- Co-authored-by: Rafael Marinho <[email protected]>
1 parent 7af3508 commit 7b5d890

File tree

9 files changed

+190
-0
lines changed

9 files changed

+190
-0
lines changed

.stylecop.ruleset

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
</Rules>
2525
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.LayoutRules">
2626
<Rule Id="SA1503" Action="None" /> <!-- Braces should not be omitted -->
27+
<Rule Id="SA1505" Action="None" /> <!-- An opening brace should not be followed by a blank line -->
28+
<Rule Id="SA1507" Action="None" /> <!-- Code should not contain multiple blank lines in a row -->
29+
<Rule Id="SA1508" Action="None" /> <!-- A closing brace should not be preceded by a blank line -->
30+
<Rule Id="SA1513" Action="None" /> <!-- A closing brace should be followed by a blank line -->
2731
<Rule Id="SA1516" Action="None" /> <!-- Elements should be separated by blank line -->
2832
<Rule Id="CS1591" Action="None" /> <!-- Missing XML comment for publicly visible type or member -->
2933
</Rules>

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ clean: ## Clean build artifacts
4949
dotnet clean
5050
rm -f $(RUNSETTINGS_FILE).tmp
5151

52+
lint: ## Run linting
53+
dotnet build --verbosity quiet
54+
5255
test_with_docker: ## Run tests in Docker (set DOTNET_VERSION to change .NET version)
5356
$(call generate_runsettings_content,$(RUNSETTINGS_FILE).tmp)
5457
docker run -t -i -w /code -v $(PWD):/code --add-host=host.docker.internal:host-gateway \

src/Clients/IUserClient.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,5 +264,20 @@ public interface IUserClient
264264
/// </summary>
265265
/// <remarks>https://getstream.io/chat/docs/javascript/block_user/</remarks>
266266
Task<ApiResponse> UnblockUserAsync(string targetId, string userID);
267+
268+
/// <summary>
269+
/// Updates a user's live location.
270+
/// </summary>
271+
/// <param name="userID">The user ID</param>
272+
/// <param name="location">The location data to update</param>
273+
/// <returns>The server response containing the updated live location share, including channel CID, message ID, user ID, latitude, longitude, device ID, end time, creation and update timestamps, and duration.</returns>
274+
Task<SharedLocationResponse> UpdateUserLiveLocationAsync(string userID, SharedLocationRequest location);
275+
276+
/// <summary>
277+
/// Gets all active live location shares for a user.
278+
/// </summary>
279+
/// <param name="userID">The user ID</param>
280+
/// <returns>The server response containing an array of active live location shares with details including channel CID, message ID, coordinates, device ID, and timestamps</returns>
281+
Task<ActiveLiveLocationsResponse> GetUserActiveLiveLocationsAsync(string userID);
267282
}
268283
}

src/Clients/UserClient.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,5 +241,34 @@ public async Task<QueryUsersResponse> QueryAsync(QueryUserOptions opts)
241241
new KeyValuePair<string, string>("payload", StreamJsonConverter.SerializeObject(payload)),
242242
});
243243
}
244+
245+
public async Task<SharedLocationResponse> UpdateUserLiveLocationAsync(string userID, SharedLocationRequest location)
246+
{
247+
if (string.IsNullOrEmpty(userID))
248+
throw new ArgumentException("User ID cannot be empty", nameof(userID));
249+
250+
return await ExecuteRequestAsync<SharedLocationResponse>("users/live_locations",
251+
HttpMethod.PUT,
252+
HttpStatusCode.Created,
253+
location,
254+
queryParams: new List<KeyValuePair<string, string>>
255+
{
256+
new KeyValuePair<string, string>("user_id", userID),
257+
});
258+
}
259+
260+
public async Task<ActiveLiveLocationsResponse> GetUserActiveLiveLocationsAsync(string userID)
261+
{
262+
if (string.IsNullOrEmpty(userID))
263+
throw new ArgumentException("User ID cannot be empty", nameof(userID));
264+
265+
return await ExecuteRequestAsync<ActiveLiveLocationsResponse>("users/live_locations",
266+
HttpMethod.GET,
267+
HttpStatusCode.OK,
268+
queryParams: new List<KeyValuePair<string, string>>
269+
{
270+
new KeyValuePair<string, string>("user_id", userID),
271+
});
272+
}
244273
}
245274
}

src/Models/ChannelConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ public class ConfigOverridesRequest
4343
public ModerationBehaviour BlocklistBehavior { get; set; }
4444
public List<string> Commands { get; set; }
4545
public bool? UserMessageReminders { get; set; }
46+
public bool? SharedLocations { get; set; }
4647
}
4748
}

src/Models/Message.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class Message : CustomDataBase
4141
public bool? Shadowed { get; set; }
4242
public Dictionary<string, List<string>> ImageLabels { get; set; }
4343
public List<string> RestrictedVisibility { get; set; }
44+
public SharedLocationRequest SharedLocation { get; set; }
4445

4546
[JsonProperty("i18n")]
4647
public Dictionary<string, string> I18n { get; set; }

src/Models/MessageRequest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class MessageRequest : CustomDataBase
3838
public UserRequest PinnedBy { get; set; }
3939
public DateTimeOffset? PinnedAt { get; set; }
4040
public IEnumerable<string> RestrictedVisibility { get; set; }
41+
public SharedLocationRequest SharedLocation { get; set; }
4142
}
4243
}
4344

src/Models/SharedLocation.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
3+
namespace StreamChat.Models
4+
{
5+
public class SharedLocationRequest
6+
{
7+
public string CreatedByDeviceId { get; set; }
8+
public DateTimeOffset? EndAt { get; set; }
9+
public double? Latitude { get; set; }
10+
public double? Longitude { get; set; }
11+
public string MessageId { get; set; }
12+
}
13+
14+
public class SharedLocationResponse : ApiResponse
15+
{
16+
public string ChannelCid { get; set; }
17+
public DateTimeOffset CreatedAt { get; set; }
18+
public string CreatedByDeviceId { get; set; }
19+
public DateTimeOffset? EndAt { get; set; }
20+
public double Latitude { get; set; }
21+
public double Longitude { get; set; }
22+
public string MessageId { get; set; }
23+
public DateTimeOffset UpdatedAt { get; set; }
24+
public string UserId { get; set; }
25+
}
26+
27+
public class ActiveLiveLocationsResponse : ApiResponse
28+
{
29+
public SharedLocationResponse[] ActiveLiveLocations { get; set; }
30+
}
31+
}

tests/UserClientTests.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,16 @@ private byte[] DecodeJwtBase64(string encodedStr)
7878
switch (encodedStr.Length % 4)
7979
{
8080
case 0:
81+
8182
// No pad chars
8283
break;
8384
case 2:
85+
8486
// Two pad chars
8587
encodedStr += "==";
8688
break;
8789
case 3:
90+
8891
// One pad char
8992
encodedStr += "=";
9093
break;
@@ -397,5 +400,107 @@ await WaitForAsync(async () =>
397400
exportFile.Should().Contain(_user1.Id);
398401
exportFile.Should().Contain(_user2.Id);
399402
}
403+
404+
[Test]
405+
public async Task TestLiveLocationAsync()
406+
{
407+
408+
var channel = await CreateChannelAsync(createdByUserId: _user1.Id, members: new[] { _user1.Id });
409+
410+
await EnableSharedLocationsAsync(channel);
411+
412+
var longitude = -122.4194;
413+
var latitude = 38.999;
414+
415+
var location = new SharedLocationRequest
416+
{
417+
Longitude = longitude,
418+
Latitude = latitude,
419+
EndAt = DateTimeOffset.UtcNow.AddHours(1),
420+
CreatedByDeviceId = "test-device",
421+
};
422+
423+
var messageRequest = new MessageRequest
424+
{
425+
Text = "Test message for shared location",
426+
SharedLocation = location,
427+
};
428+
var messageResp = await _messageClient.SendMessageAsync(
429+
channel.Type,
430+
channel.Id,
431+
messageRequest,
432+
_user1.Id);
433+
434+
var message = messageResp.Message;
435+
436+
var newLongitude = -122.4194;
437+
var newLatitude = 38.999;
438+
439+
var newLocation = new SharedLocationRequest
440+
{
441+
MessageId = message.Id,
442+
Longitude = newLongitude,
443+
Latitude = newLatitude,
444+
EndAt = DateTimeOffset.UtcNow.AddHours(10),
445+
CreatedByDeviceId = "test-device",
446+
};
447+
448+
SharedLocationResponse updateResp;
449+
updateResp = await _userClient.UpdateUserLiveLocationAsync(_user1.Id, newLocation);
450+
451+
updateResp.Should().NotBeNull();
452+
updateResp.Latitude.Should().Be(newLatitude);
453+
updateResp.Longitude.Should().Be(newLongitude);
454+
455+
var getResp = await _userClient.GetUserActiveLiveLocationsAsync(_user1.Id);
456+
457+
getResp.Should().NotBeNull();
458+
getResp.ActiveLiveLocations.Should().NotBeEmpty("Should have active live locations");
459+
460+
var newLocationResp = getResp.ActiveLiveLocations.FirstOrDefault(loc => loc.MessageId == message.Id);
461+
newLocationResp.Should().NotBeNull();
462+
newLocationResp.Latitude.Should().Be(newLatitude);
463+
newLocationResp.Longitude.Should().Be(newLongitude);
464+
}
465+
466+
/// <summary>
467+
/// Enables shared locations for the channel by updating config overrides.
468+
/// </summary>
469+
private async Task EnableSharedLocationsAsync(ChannelWithConfig channel)
470+
{
471+
var request = new PartialUpdateChannelRequest
472+
{
473+
Set = new Dictionary<string, object>
474+
{
475+
{
476+
"config_overrides", new Dictionary<string, object>
477+
{
478+
{ "shared_locations", true },
479+
}
480+
},
481+
},
482+
};
483+
await _channelClient.PartialUpdateAsync(channel.Type, channel.Id, request);
484+
}
485+
486+
/// <summary>
487+
/// Disables shared locations for the channel by updating config overrides.
488+
/// </summary>
489+
private async Task DisableSharedLocationsAsync(ChannelWithConfig channel)
490+
{
491+
var request = new PartialUpdateChannelRequest
492+
{
493+
Set = new Dictionary<string, object>
494+
{
495+
{
496+
"config_overrides", new Dictionary<string, object>
497+
{
498+
{ "shared_locations", false },
499+
}
500+
},
501+
},
502+
};
503+
await _channelClient.PartialUpdateAsync(channel.Type, channel.Id, request);
504+
}
400505
}
401506
}

0 commit comments

Comments
 (0)