diff --git a/README.md b/README.md
index 6f64007..b845e8a 100644
--- a/README.md
+++ b/README.md
@@ -268,6 +268,16 @@ You can link a private Telegram group to a ride so that participants are automat
**Note:** Telegram does not allow bots to add users directly — participants receive a private invite link instead.
+### Joining the Ride Group Chat
+
+Once a group is attached, the ride message displays a notice with instructions. Any participant who has joined the ride can request an invite link by sending the bot a private message:
+
+```
+/joinchat #rideId
+```
+
+The bot sends a single-use invite link valid for 24 hours. The command only works if you have joined the ride.
+
## Route Support
The bot supports route links from:
diff --git a/SPECIFICATION.MD b/SPECIFICATION.MD
index 28bc04e..5314187 100644
--- a/SPECIFICATION.MD
+++ b/SPECIFICATION.MD
@@ -124,7 +124,7 @@ All handlers extend **[BaseCommandHandler](cci:2://file:///workspace/src/command
- **ListRidesCommandHandler**: Paginated list of user's rides
- **ListParticipantsCommandHandler**: List all participants for a specific ride (shows all without truncation, organized by participation state)
- **ParticipationHandlers**: Join/thinking/pass ride functionality; triggers group membership sync via `GroupManagementService` when a group is attached to the ride
-- **GroupCommandHandler**: `/attach #rideId` (links a group to a ride, posts and pins the ride message, adds existing participants) and `/detach` (unlinks the group); both commands are group-chat-only
+- **GroupCommandHandler**: `/attach #rideId` (links a group to a ride, posts and pins the ride message, adds existing participants, updates all existing ride messages), `/detach` (unlinks the group, updates all existing ride messages), and `/joinchat #rideId` (private-only: sends an invite link to the ride's group chat if the user has joined the ride); attach/detach are group-chat-only
- **StartCommandHandler**: Welcome message
- **HelpCommandHandler**: Multi-page help system
- **FromStravaCommandHandler**: Import or update a ride from a Strava club event URL; uses `StravaEventParser` to fetch event data and maps it to ride fields; repeated calls with the same URL by the same user update the existing ride
@@ -149,6 +149,7 @@ All handlers extend **[BaseCommandHandler](cci:2://file:///workspace/src/command
- Formats ride lists with pagination
- Handles date/time formatting with timezone support
- **Share line for creators**: Shows "Share this ride: `/shareride #ID`" for ride creators in private chats
+- **Group chat line**: When a group is attached to a ride (`ride.groupId` is set), shows a notice with `/joinchat #ID` instructions in all ride messages; line is absent (no extra whitespace) when no group is attached
- Groups ride details logically
### **8. Utilities ([/src/utils/](cci:7://file:///workspace/src/utils:0:0-0:0))**
diff --git a/docs/changes/joinchat-group-chat-line-specification.md b/docs/changes/joinchat-group-chat-line-specification.md
new file mode 100644
index 0000000..b2d15d0
--- /dev/null
+++ b/docs/changes/joinchat-group-chat-line-specification.md
@@ -0,0 +1,168 @@
+# Specification: /joinchat Command and Group Chat Line in Ride Messages
+
+## Overview
+
+This change extends the group attachment feature with two improvements:
+
+1. **Group chat line in ride messages** — when a group is attached to a ride, all ride messages (in all chats) display a notice telling participants how to join the group chat. The line disappears when the group is detached. Attaching or detaching a group triggers an update of all existing ride messages.
+
+2. **`/joinchat` command** — a new private-only command that lets a ride participant request an invite link to the ride's group chat directly from the bot.
+
+---
+
+## Ride Message: Group Chat Line
+
+### When shown
+
+The line appears **only when `ride.groupId` is set**. When no group is attached, the line is absent with no extra blank lines.
+
+### Placement
+
+After the participants section (joined / thinking / not interested), before the footer (`🎫 #Ride #ID`), with one blank line between the participants section and the notice, and one blank line between the notice and the footer:
+
+```
+🙅 Not interested: 3
+
+Join the ride's private group chat: send /joinchat #abc123 to the bot in private messages (only works if you have joined the ride).
+
+🎫 #Ride #abc123
+```
+
+Without a group:
+
+```
+🙅 Not interested: 3
+
+🎫 #Ride #abc123
+```
+
+### Template change
+
+The `templates.ride` i18n key gets a `{groupChatLine}` placeholder inserted between the blank line and `{shareLine}`:
+
+```
+{groupChatLine}{shareLine}🎫 #Ride #{id}{cancelledInstructions}
+```
+
+### Formatter logic (`MessageFormatter.formatRideMessage`)
+
+```javascript
+const groupChatLine = ride.groupId
+ ? `${this.translate('formatter.groupChatLine', { id: ride.id }, language)}\n\n`
+ : '';
+message = message.replace('{groupChatLine}', groupChatLine);
+```
+
+### Message update on attach / detach
+
+- **`/attach`**: after saving `groupId`, all pre-existing ride messages are updated via `RideMessagesService.updateRideMessages`. The new message posted in the group is created with the `groupId` already set, so it immediately shows the notice.
+- **`/detach`**: after clearing `groupId`, all ride messages are updated to remove the notice.
+
+Both updates are best-effort (wrapped in try/catch with logging).
+
+---
+
+## `/joinchat` Command
+
+### Context
+
+**Private chat only.** (Registered in `Bot.js` under `privateOnly` commands.)
+
+### Arguments
+
+`rideId` — the ride ID, optionally prefixed with `#`. Parsed via the existing `RideMessagesService.extractRideId` utility.
+
+### Success flow
+
+1. Parse ride ID from command text
+2. Fetch ride by ID — error if not found
+3. Check `ride.groupId` is set — error if not
+4. Check caller (`ctx.from.id`) is in `ride.participation.joined` — error if not
+5. Call `GroupManagementService.addParticipant(ctx.api, ride.groupId, ctx.from.id, ctx.lang, ride.createdBy)`
+ - Unbans the user (so previously-kicked users can accept a fresh link)
+ - Creates a single-use invite link (24-hour expiry)
+ - Sends the link to the user via DM
+ - If the DM fails with 403 (user hasn't started the bot): notifies the ride creator instead
+
+No reply is sent by the handler itself; `addParticipant` handles all messaging.
+
+### Error cases
+
+| Condition | i18n key |
+|---|---|
+| Ride ID missing or invalid | `commands.group.invalidRideIdUsage` |
+| Ride not found | `commands.group.rideNotFound` |
+| Ride has no attached group | `commands.group.joinchatNoGroup` |
+| Caller not in `joined` list | `commands.group.joinchatNotParticipant` |
+
+---
+
+## i18n Keys
+
+### New keys
+
+#### `formatter.groupChatLine`
+
+| Locale | Value |
+|---|---|
+| EN | `Join the ride's private group chat: send /joinchat #{id} to the bot in private messages (only works if you have joined the ride).` |
+| RU | `Присоединяйтесь к закрытой группе поездки: напишите /joinchat #{id} боту в личные сообщения (работает только если вы записались в поездку).` |
+
+#### `commands.group.joinchatNoGroup`
+
+| Locale | Value |
+|---|---|
+| EN | `This ride doesn't have an attached group chat.` |
+| RU | `К этой поездке не привязана группа.` |
+
+#### `commands.group.joinchatNotParticipant`
+
+| Locale | Value |
+|---|---|
+| EN | `You need to join the ride first.` |
+| RU | `Сначала нужно записаться в поездку.` |
+
+#### `bot.commandDescriptions.joinchat`
+
+| Locale | Value |
+|---|---|
+| EN | `Join the private group chat for a ride` |
+| RU | `Войти в закрытую группу поездки` |
+
+### Updated `help2` template
+
+Both locales get a new **💬 Joining the Ride Group Chat** section appended after the group attachment section.
+
+---
+
+## Implementation Details
+
+### `GroupCommandHandler.handleAttach` changes
+
+- Builds a local `rideWithGroupId = { ...ride, groupId }` after `updateRide` so that `createRideMessage` uses the ride object with `groupId` set (ensuring the new group message immediately shows the notice)
+- After the participant-add loop, fetches the latest ride from storage and calls `updateRideMessages` (best-effort)
+
+### `GroupCommandHandler.handleDetach` changes
+
+- After `updateRide({ groupId: null })`, calls `updateRideMessages({ ...ride, groupId: null }, ctx)` (best-effort) before sending the success reply
+
+### `GroupCommandHandler.handleJoinChat` (new method)
+
+Delegates to `GroupManagementService.addParticipant` — no new membership logic is introduced; the same invite-link mechanism used by `ParticipationHandlers` is reused.
+
+---
+
+## Files Changed
+
+| File | Change |
+|---|---|
+| `src/i18n/locales/en.js` | Add `formatter.groupChatLine`, `commands.group.joinchatNoGroup`, `commands.group.joinchatNotParticipant`, `bot.commandDescriptions.joinchat`; add `{groupChatLine}` to `templates.ride`; update `help2` |
+| `src/i18n/locales/ru.js` | Same as EN |
+| `src/formatters/MessageFormatter.js` | Compute and replace `{groupChatLine}` in `formatRideMessage` |
+| `src/commands/GroupCommandHandler.js` | Update `handleAttach` (use `rideWithGroupId`, call `updateRideMessages` after attach); update `handleDetach` (call `updateRideMessages` before reply); add `handleJoinChat` |
+| `src/core/Bot.js` | Register `/joinchat` as a `privateOnly` command |
+| `src/__tests__/commands/group-command-handler.test.js` | Update attach/detach assertions; add `handleJoinChat` test suite |
+| `src/__tests__/core/bot.test.js` | Update private command count (11 → 12) |
+| `README.md` | Add "Joining the Ride Group Chat" section |
+| `SPECIFICATION.MD` | Update `GroupCommandHandler` entry; add group chat line to formatter description |
+| `docs/changes/joinchat-group-chat-line-specification.md` | This document |
diff --git a/src/__tests__/commands/group-command-handler.test.js b/src/__tests__/commands/group-command-handler.test.js
index d0c0ea1..81e0bcb 100644
--- a/src/__tests__/commands/group-command-handler.test.js
+++ b/src/__tests__/commands/group-command-handler.test.js
@@ -138,14 +138,20 @@ describe.each(['en', 'ru'])('GroupCommandHandler (%s)', (language) => {
expect(mockCtx.reply).toHaveBeenCalledWith(tr('commands.group.botNeedsAddMembersPermission'));
});
- it('should attach group, post message, pin it, and reply success', async () => {
+ it('should attach group, post message, pin it, update messages, and reply success', async () => {
mockRideService.getRide.mockResolvedValue(makeRide());
await handler.handleAttach(mockCtx);
expect(mockRideService.updateRide).toHaveBeenCalledWith(RIDE_ID, { groupId: GROUP_ID });
- expect(mockRideMessagesService.createRideMessage).toHaveBeenCalled();
+ expect(mockRideMessagesService.createRideMessage).toHaveBeenCalledWith(
+ expect.objectContaining({ id: RIDE_ID, groupId: GROUP_ID }),
+ mockCtx,
+ null
+ );
expect(mockCtx.api.pinChatMessage).toHaveBeenCalledWith(GROUP_ID, 777, { disable_notification: true });
+ expect(mockRideService.getRide).toHaveBeenCalledTimes(2);
+ expect(mockRideMessagesService.updateRideMessages).toHaveBeenCalled();
expect(mockCtx.reply).toHaveBeenCalledWith(tr('commands.group.attachSuccess'));
});
@@ -226,6 +232,10 @@ describe.each(['en', 'ru'])('GroupCommandHandler (%s)', (language) => {
await handler.handleDetach(mockCtx);
expect(mockRideService.updateRide).toHaveBeenCalledWith(RIDE_ID, { groupId: null });
+ expect(mockRideMessagesService.updateRideMessages).toHaveBeenCalledWith(
+ expect.objectContaining({ id: RIDE_ID, groupId: null }),
+ mockCtx
+ );
expect(mockCtx.reply).toHaveBeenCalledWith(tr('commands.group.detachSuccess'));
});
@@ -237,6 +247,10 @@ describe.each(['en', 'ru'])('GroupCommandHandler (%s)', (language) => {
await handler.handleDetach(mockCtx);
expect(mockRideService.updateRide).toHaveBeenCalledWith(RIDE_ID, { groupId: null });
+ expect(mockRideMessagesService.updateRideMessages).toHaveBeenCalledWith(
+ expect.objectContaining({ id: RIDE_ID, groupId: null }),
+ mockCtx
+ );
expect(mockCtx.reply).toHaveBeenCalledWith(tr('commands.group.detachSuccess'));
});
@@ -251,4 +265,59 @@ describe.each(['en', 'ru'])('GroupCommandHandler (%s)', (language) => {
expect(mockRideService.updateRide).not.toHaveBeenCalled();
});
});
+
+ // ─── /joinchat tests ──────────────────────────────────────────────────────────
+
+ describe('handleJoinChat', () => {
+ beforeEach(() => {
+ mockCtx.chat.type = 'private';
+ mockCtx.message.text = `/joinchat #${RIDE_ID}`;
+ });
+
+ it('should reply if ride not found', async () => {
+ mockRideService.getRide.mockResolvedValue(null);
+
+ await handler.handleJoinChat(mockCtx);
+
+ expect(mockCtx.reply).toHaveBeenCalledWith(tr('commands.group.rideNotFound'));
+ expect(mockGroupManagementService.addParticipant).not.toHaveBeenCalled();
+ });
+
+ it('should reply if ride has no attached group', async () => {
+ mockRideService.getRide.mockResolvedValue(makeRide({ groupId: null }));
+
+ await handler.handleJoinChat(mockCtx);
+
+ expect(mockCtx.reply).toHaveBeenCalledWith(tr('commands.group.joinchatNoGroup'));
+ expect(mockGroupManagementService.addParticipant).not.toHaveBeenCalled();
+ });
+
+ it('should reply if user has not joined the ride', async () => {
+ mockRideService.getRide.mockResolvedValue(makeRide({ groupId: GROUP_ID }));
+
+ await handler.handleJoinChat(mockCtx);
+
+ expect(mockCtx.reply).toHaveBeenCalledWith(tr('commands.group.joinchatNotParticipant'));
+ expect(mockGroupManagementService.addParticipant).not.toHaveBeenCalled();
+ });
+
+ it('should call addParticipant if user has joined the ride', async () => {
+ const ride = makeRide({
+ groupId: GROUP_ID,
+ participation: {
+ joined: [{ userId: CREATOR_ID, firstName: 'Creator', lastName: '', username: 'creator' }],
+ thinking: [],
+ skipped: []
+ }
+ });
+ mockRideService.getRide.mockResolvedValue(ride);
+
+ await handler.handleJoinChat(mockCtx);
+
+ expect(mockGroupManagementService.addParticipant).toHaveBeenCalledWith(
+ mockCtx.api, GROUP_ID, CREATOR_ID, language, CREATOR_ID
+ );
+ expect(mockCtx.reply).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/__tests__/core/bot.test.js b/src/__tests__/core/bot.test.js
index c215321..53a1e65 100644
--- a/src/__tests__/core/bot.test.js
+++ b/src/__tests__/core/bot.test.js
@@ -69,7 +69,7 @@ describe('Bot', () => {
expect(bot.botConfig.callbacks).toBeInstanceOf(Array);
// Check that commands are properly configured
- expect(bot.botConfig.commands.privateOnly.length).toBe(12); // 12 private commands
+ expect(bot.botConfig.commands.privateOnly.length).toBe(13); // 13 private commands
expect(bot.botConfig.commands.mixed.length).toBe(1); // 1 mixed command (shareride)
expect(bot.botConfig.callbacks.length).toBe(7); // 7 callback handlers
});
diff --git a/src/commands/GroupCommandHandler.js b/src/commands/GroupCommandHandler.js
index 2b4c8f4..e761ad3 100644
--- a/src/commands/GroupCommandHandler.js
+++ b/src/commands/GroupCommandHandler.js
@@ -87,6 +87,7 @@ export class GroupCommandHandler extends BaseCommandHandler {
// Save groupId to ride
await this.rideService.updateRide(rideId, { groupId });
+ const rideWithGroupId = { ...ride, groupId };
// Rename the group (best-effort)
try {
@@ -100,7 +101,7 @@ export class GroupCommandHandler extends BaseCommandHandler {
// Post and pin the ride message in the group
try {
- const { sentMessage } = await this.rideMessagesService.createRideMessage(ride, ctx, ctx.message?.message_thread_id);
+ const { sentMessage } = await this.rideMessagesService.createRideMessage(rideWithGroupId, ctx, ctx.message?.message_thread_id);
if (sentMessage?.message_id) {
try {
await ctx.api.pinChatMessage(groupId, sentMessage.message_id, { disable_notification: true });
@@ -118,9 +119,52 @@ export class GroupCommandHandler extends BaseCommandHandler {
await this.groupManagementService.addParticipant(ctx.api, groupId, participant.userId, ctx.lang, ride.createdBy);
}
+ // Update all pre-existing ride messages to show the group chat line (best-effort)
+ try {
+ const latestRide = await this.rideService.getRide(rideId);
+ if (latestRide) await this.rideMessagesService.updateRideMessages(latestRide, ctx);
+ } catch (updateError) {
+ console.error('GroupCommandHandler: failed to update ride messages after attach:', updateError);
+ }
+
await ctx.reply(this.translate(ctx, 'commands.group.attachSuccess'));
}
+ /**
+ * Handle /joinchat #rideId — sends an invite link to join the ride's group chat
+ * @param {import('grammy').Context} ctx
+ */
+ async handleJoinChat(ctx) {
+ const { rideId, error } = this.rideMessagesService.extractRideId(ctx.message, ctx.lang ? { language: ctx.lang } : {});
+ if (!rideId) {
+ await ctx.reply(error || this.translate(ctx, 'commands.group.invalidRideIdUsage'));
+ return;
+ }
+
+ let ride;
+ try {
+ ride = await this.rideService.getRide(rideId);
+ } catch (e) {
+ ride = null;
+ }
+ if (!ride) {
+ await ctx.reply(this.translate(ctx, 'commands.group.rideNotFound'));
+ return;
+ }
+ if (!ride.groupId) {
+ await ctx.reply(this.translate(ctx, 'commands.group.joinchatNoGroup'));
+ return;
+ }
+
+ const isParticipant = (ride.participation?.joined || []).some(p => p.userId === ctx.from.id);
+ if (!isParticipant) {
+ await ctx.reply(this.translate(ctx, 'commands.group.joinchatNotParticipant'));
+ return;
+ }
+
+ await this.groupManagementService.addParticipant(ctx.api, ride.groupId, ctx.from.id, ctx.lang, ride.createdBy);
+ }
+
/**
* Handle /detach — unlinks the current group from its ride
* @param {import('grammy').Context} ctx
@@ -162,6 +206,11 @@ export class GroupCommandHandler extends BaseCommandHandler {
}
await this.rideService.updateRide(ride.id, { groupId: null });
+ try {
+ await this.rideMessagesService.updateRideMessages({ ...ride, groupId: null }, ctx);
+ } catch (updateError) {
+ console.error('GroupCommandHandler: failed to update ride messages after detach:', updateError);
+ }
await ctx.reply(this.translate(ctx, 'commands.group.detachSuccess'));
}
}
diff --git a/src/core/Bot.js b/src/core/Bot.js
index 3a111ad..9e8583f 100644
--- a/src/core/Bot.js
+++ b/src/core/Bot.js
@@ -84,6 +84,7 @@ export class Bot {
{ command: 'dupride', descriptionKey: 'bot.commandDescriptions.dupride', handler: (ctx) => duplicateRideHandler.handle(ctx) },
{ command: 'resumeride', descriptionKey: 'bot.commandDescriptions.resumeride', handler: (ctx) => resumeRideHandler.handle(ctx) },
{ command: 'airide', descriptionKey: 'bot.commandDescriptions.airide', handler: (ctx) => this.aiRideHandler.handle(ctx) },
+ { command: 'joinchat', descriptionKey: 'bot.commandDescriptions.joinchat', handler: (ctx) => groupHandler.handleJoinChat(ctx) },
{ command: 'fromstrava', descriptionKey: 'bot.commandDescriptions.fromstrava', handler: (ctx) => this.fromStravaHandler.handle(ctx) },
],
publicOnly: [
diff --git a/src/formatters/MessageFormatter.js b/src/formatters/MessageFormatter.js
index 6948227..2c11bad 100644
--- a/src/formatters/MessageFormatter.js
+++ b/src/formatters/MessageFormatter.js
@@ -200,6 +200,11 @@ export class MessageFormatter {
: '';
message = message.replace('{shareLine}', shareLine);
+ const groupChatLine = ride.groupId
+ ? `${this.translate('formatter.groupChatLine', { id: ride.id }, language)}\n\n`
+ : '';
+ message = message.replace('{groupChatLine}', groupChatLine);
+
message = message.replace('{id}', ride.id);
// Remove lines that contain only emoji and empty content (e.g., "🤔 Thinking (0): ")
diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js
index 4107be7..ff1654e 100644
--- a/src/i18n/locales/en.js
+++ b/src/i18n/locales/en.js
@@ -185,6 +185,12 @@ Only the ride creator can attach a group:
The bot will post the ride info in the group and automatically add/remove members as participants join or leave the ride.
To unlink the group, use /detach in the group chat.
+💬 Joining the Ride Group Chat
+Once a group is attached to a ride, the ride message shows a notice with instructions.
+Any participant who has joined the ride can request an invite link by sending the bot a private message:
+/joinchat #rideId
+The bot will send you a single-use invite link (valid 24 hours). The command only works if you have joined the ride.
+
`.trim(),
ride: `
@@ -195,7 +201,7 @@ To unlink the group, use /detach in the group chat.
🤔 {thinkingLabel} ({thinkingCount}): {thinking}
🙅 {notInterestedLabel}: {notInterestedCount}
-{shareLine}🎫 #Ride #{id}{cancelledInstructions}
+{groupChatLine}{shareLine}🎫 #Ride #{id}{cancelledInstructions}
`.trim(),
cancelled: '❌ CANCELLED',
@@ -328,6 +334,8 @@ Click here to start a private chat: @botname
inviteLinkSent: 'You\'ve been invited to the ride group: {link}\n\nThis group is for ride coordination, pre- and post-ride discussion, and sharing photos. The link is valid for 24 hours.',
inviteLinkForCreator: 'A participant couldn\'t receive the group invite link automatically — they haven\'t started a conversation with the bot. Please forward this link to them manually: {link}',
invalidRideIdUsage: 'Please provide a valid ride ID. Usage: /attach #rideID',
+ joinchatNoGroup: 'This ride doesn\'t have an attached group chat.',
+ joinchatNotParticipant: 'You need to join the ride first.',
chatTitle: 'Ride: {title} @ {date}'
},
delete: {
@@ -377,6 +385,7 @@ Click here to start a private chat: @botname
andMoreParticipants: '{displayedList} and {count} more',
upToSpeed: 'up to {max} km/h',
shareLine: 'Share this ride: /shareride #{id}',
+ groupChatLine: '
Join the ride\'s private group chat: send /joinchat #{id} to the bot in private messages (only works if you have joined the ride).',
labels: {
when: 'When',
category: 'Category',
@@ -505,6 +514,7 @@ Click here to start a private chat: @botname
attach: 'Attach a Telegram group to a ride',
detach: 'Detach the Telegram group from its ride',
airide: 'Create or update a ride using AI',
+ joinchat: 'Join the private group chat for a ride',
fromstrava: 'Create or update a ride from a Strava event'
}
}
diff --git a/src/i18n/locales/ru.js b/src/i18n/locales/ru.js
index eb285f8..67eb841 100644
--- a/src/i18n/locales/ru.js
+++ b/src/i18n/locales/ru.js
@@ -185,6 +185,12 @@ id: abc123 (or #abc123)
Бот опубликует информацию о поездке в группе и будет автоматически добавлять/удалять участников по мере изменения их статуса.
Чтобы отвязать группу, используйте /detach в группе.
+💬 Вступление в группу поездки
+Когда группа привязана к поездке, в сообщении о поездке появляется подсказка.
+Любой участник, записавшийся в поездку, может запросить ссылку-приглашение, написав боту в личные сообщения:
+/joinchat #rideId
+Бот вышлет одноразовую ссылку (действительна 24 часа). Команда работает только если вы записались в поездку.
+
`.trim(),
ride: `
@@ -195,7 +201,7 @@ id: abc123 (or #abc123)
🤔 {thinkingLabel} ({thinkingCount}): {thinking}
🙅 {notInterestedLabel}: {notInterestedCount}
-{shareLine}🎫 #Ride #{id}{cancelledInstructions}
+{groupChatLine}{shareLine}🎫 #Ride #{id}{cancelledInstructions}
`.trim(),
cancelled: '❌ ОТМЕНЕНО',
@@ -328,6 +334,8 @@ id: abc123 (or #abc123)
inviteLinkSent: 'Вы приглашены в группу поездки: {link}\n\nЭта группа предназначена для организации поездки, общения до и после неё, а также обмена фотографиями. Ссылка действительна 24 часа.',
inviteLinkForCreator: 'Участник не смог получить ссылку-приглашение автоматически — он ещё не начал разговор с ботом. Пожалуйста, отправьте ему эту ссылку вручную: {link}',
invalidRideIdUsage: 'Укажите корректный ID поездки. Использование: /attach #rideID',
+ joinchatNoGroup: 'К этой поездке не привязана группа.',
+ joinchatNotParticipant: 'Сначала нужно записаться в поездку.',
chatTitle: 'Поездка: {title} @ {date}'
},
delete: {
@@ -377,6 +385,7 @@ id: abc123 (or #abc123)
andMoreParticipants: '{displayedList} и еще {count}',
upToSpeed: 'до {max} км/ч',
shareLine: 'Поделиться поездкой: /shareride #{id}',
+ groupChatLine: 'Присоединяйтесь к закрытой группе поездки: напишите /joinchat #{id} боту в личные сообщения (работает только если вы записались в поездку).',
labels: {
when: 'Когда',
category: 'Категория',
@@ -505,6 +514,7 @@ id: abc123 (or #abc123)
attach: 'Привязать Telegram-группу к поездке',
detach: 'Отвязать Telegram-группу от поездки',
airide: 'Создать или обновить поездку с помощью AI',
+ joinchat: 'Войти в закрытую группу поездки',
fromstrava: 'Создать или обновить поездку из события Strava'
}
}