src/Controller/Api/ConversationsController.php line 564

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api;
  3. use App\Entity\Core\Notifications;
  4. use App\Entity\Core\Users;
  5. use App\Entity\Messenger\Groups;
  6. use App\Entity\Messenger\Messages;
  7. use App\Services\Dossiers;
  8. use App\Services\NotificationService;
  9. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  10. use Symfony\Component\HttpFoundation\Request;
  11. use Symfony\Component\HttpFoundation\Response;
  12. use Symfony\Component\HttpFoundation\JsonResponse;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use Knp\Component\Pager\PaginatorInterface;
  15. class ConversationsController extends AbstractController
  16. {
  17. private $em;
  18. private $paginator;
  19. private $dossier;
  20. private $notificationService;
  21. public function __construct(EntityManagerInterface $em,
  22. Dossiers $dossier,
  23. PaginatorInterface $paginator,
  24. NotificationService $notificationService
  25. ) {
  26. $this->em = $em;
  27. $this->paginator = $paginator;
  28. $this->dossier = $dossier;
  29. $this->notificationService = $notificationService;
  30. }
  31. /**
  32. * Liste des conversations récentes de l'utilisateur connecté
  33. */
  34. public function recents(Request $request): JsonResponse
  35. {
  36. $user = $this->getUser();
  37. $limit = $request->query->get('limit', 20);
  38. $conversations = $this->em->getRepository(Groups::class)->recents($user, $limit);
  39. $array = [];
  40. foreach ($conversations as $conversation) {
  41. $otherUser = $conversation->getOtherUser($user);
  42. if (!$otherUser) {
  43. continue;
  44. }
  45. $countMessages = $this->em->getRepository(Messages::class)->countByConversation($conversation);
  46. $fullname = $otherUser->getName()." ". $otherUser->getLastname();
  47. $avatar = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNjY3ZWVhO3N0b3Atb3BhY2l0eToxIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiM3NjRiYTI7c3RvcC1vcGFjaXR5OjEiIC8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Y2lyY2xlIGN4PSIxMDAiIGN5PSIxMDAiIHI9IjEwMCIgZmlsbD0idXJsKCNncmFkKSIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCwgMTAwKSI+CiAgICA8Y2lyY2xlIGN4PSIwIiBjeT0iLTE1IiByPSIzNSIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgICA8cGF0aCBkPSJNIC01MCA2NSBRIC01MCAyNSAtMzUgMTAgUSAtMTUgLTUgMCAtNSBRIDE1IC01IDM1IDEwIFEgNTAgMjUgNTAgNjUgWiIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgPC9nPgo8L3N2Zz4K";
  48. if ($otherUser->getImage()) {
  49. $avatar = $otherUser->getImageBase64();
  50. }
  51. $array[] = [
  52. "id" => (string)$conversation->getId(),
  53. "conversationHash" => $conversation->getConversationHash(),
  54. "title" => $conversation->getTitle() ?: 'Conversation avec ' . $fullname,
  55. "fullname" => $fullname,
  56. "userId" => $otherUser->getId(),
  57. "userEmail" => $otherUser->getEmail(),
  58. "avatar" => $avatar,
  59. "count_messages" => $countMessages,
  60. "open" => false,
  61. "lastmessage" => $conversation->getLastMessageContent() ?? '',
  62. "lastMessageAt" => $conversation->getLastMessageAt() ? $conversation->getLastMessageAt()->format('c') : null,
  63. "createdAt" => $conversation->getCreatedAt() ? $conversation->getCreatedAt()->format('c') : null,
  64. ];
  65. }
  66. return new JsonResponse($array);
  67. }
  68. /**
  69. * Afficher les messages d'une conversation
  70. */
  71. public function show(Request $request, int $id): JsonResponse
  72. {
  73. $user = $this->getUser();
  74. $conversation = $this->em->getRepository(Groups::class)->find($id);
  75. if (!$conversation) {
  76. return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  77. }
  78. if (!$conversation->hasUser($user)) {
  79. return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  80. }
  81. $limit = $request->query->get('limit', 100);
  82. $offset = $request->query->get('offset', 0);
  83. $messages = $this->em->getRepository(Messages::class)->findByConversation($conversation, $limit, $offset);
  84. $array = [];
  85. foreach ($messages as $message) {
  86. $isOwn = $message->getUser()->getId() === $user->getId();
  87. $fullname = $message->getUser()->getName()." ". $message->getUser()->getLastname();
  88. $avatar = $this->getDefaultAvatar();
  89. if ($message->getUser()->getImage()) {
  90. $avatar = $message->getUser()->getImageBase64();
  91. }
  92. $array[] = [
  93. "id" => (string)$message->getId(),
  94. "message" => (string)$message->getMessage(),
  95. "isOwn" => $isOwn,
  96. "userId" => $message->getUser()->getId(),
  97. "userName" => $fullname,
  98. "avatar" => $avatar,
  99. "createdAt" => $message->getCreatedAt() ? $message->getCreatedAt()->format('c') : null,
  100. ];
  101. }
  102. return new JsonResponse($array);
  103. }
  104. /**
  105. * Envoyer un message
  106. */
  107. public function sendMessage(Request $request, int $id): JsonResponse
  108. {
  109. $user = $this->getUser();
  110. $conversation = $this->em->getRepository(Groups::class)->find($id);
  111. if (!$conversation) {
  112. return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  113. }
  114. if (!$conversation->hasUser($user)) {
  115. return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  116. }
  117. $data = json_decode($request->getContent(), true);
  118. $messageText = $data['message'] ?? null;
  119. if (empty($messageText)) {
  120. return new JsonResponse(['error' => 'Message vide'], Response::HTTP_BAD_REQUEST);
  121. }
  122. $emetteur = $user;
  123. $destinataire = null;
  124. if($conversation->getUserOne() !== $user) {
  125. $destinataire = $conversation->getUserOne();
  126. }
  127. if($conversation->getUserTwo() !== $user) {
  128. $destinataire = $conversation->getUserTwo();
  129. }
  130. // Créer le message
  131. $message = new Messages();
  132. $message->setMessage($messageText);
  133. $message->setUser($user);
  134. $message->setGroup($conversation);
  135. $message->setCreatedAt(new \DateTime());
  136. $this->em->persist($message);
  137. $this->em->flush();
  138. // Mettre à jour la conversation (avec le nouveau champ lastMessageSenderId)
  139. $conversation->setLastMessageContent(substr($messageText, 0, 100));
  140. $conversation->setLastMessageId($message->getId());
  141. $conversation->setLastMessageAt(new \DateTime());
  142. $conversation->setLastMessageSenderId($user->getId()); // ← NOUVEAU
  143. $this->em->persist($conversation);
  144. $this->em->flush();
  145. // Vers le destinataire
  146. $notification = new Notifications();
  147. $notification->setViewed(false);
  148. $notification->setOnlyAdmin(false);
  149. $notification->setTitle("message");
  150. if($conversation->getUserOne()->getLanguage() == "fr") {
  151. $notification->setDescription("Message reçu par ".$emetteur->getName()." ".$emetteur->getLastname());
  152. $this->notificationService->sendToUser(
  153. $destinataire,
  154. 'Nouveau message',
  155. $this->cleanMessageNotification($message->getMessage()),
  156. ['type' => 'message', 'conversationId' => $conversation->getId()],
  157. );
  158. } else {
  159. $this->notificationService->sendToUser(
  160. $destinataire,
  161. 'New message',
  162. $this->cleanMessageNotification($message->getMessage()),
  163. ['type' => 'message', 'conversationId' => $conversation->getId()],
  164. );
  165. $notification->setDescription("Message received by ".$emetteur->getName()." ".$emetteur->getLastname());
  166. }
  167. $notification->setType("default");
  168. $notification->setUser($conversation->getUserTwo());
  169. $this->em->persist($notification);
  170. $this->em->flush();
  171. $fullname = $user->getName()." ".$user->getLastname();
  172. $avatar = $this->getDefaultAvatar();
  173. if ($user->getImage()) {
  174. $avatar = $user->getImageBase64();
  175. }
  176. try {
  177. $client = new \GuzzleHttp\Client([
  178. 'timeout' => 5.0,
  179. 'verify' => false,
  180. ]);
  181. $websocketUrl = $_ENV["WEBSOCKET_HTTP_URL"].'/broadcast';
  182. error_log("╔═══════════════════════════════════════════════════════╗");
  183. error_log("║ 📡 BROADCAST WEBSOCKET - ENVOI MESSAGE ║");
  184. error_log("╚═══════════════════════════════════════════════════════╝");
  185. error_log("🔡 [WebSocket] URL cible : " . $websocketUrl);
  186. error_log("🔡 [WebSocket] Group ID : " . $conversation->getId());
  187. error_log("🔡 [WebSocket] Message ID: " . $message->getId());
  188. error_log("🔡 [WebSocket] User ID : " . $user->getId());
  189. error_log("🔡 [WebSocket] Fullname : " . $fullname);
  190. $broadcastData = [
  191. 'groupId' => (int) $conversation->getId(),
  192. 'message' => [
  193. 'id' => (int) $message->getId(),
  194. 'message' => $message->getMessage(),
  195. 'isOwn' => false,
  196. 'userId' => $user->getId(),
  197. 'userName' => $fullname,
  198. 'avatar' => $avatar,
  199. 'createdAt' => $message->getCreatedAt()->format('c'),
  200. ]
  201. ];
  202. error_log("📤 [WebSocket] Données envoyées: " . json_encode($broadcastData));
  203. $response = $client->post($websocketUrl, [
  204. 'json' => $broadcastData,
  205. 'headers' => [
  206. 'Content-Type' => 'application/json',
  207. ]
  208. ]);
  209. $statusCode = $response->getStatusCode();
  210. $body = $response->getBody()->getContents();
  211. $bodyData = json_decode($body, true);
  212. error_log("✅ [WebSocket] Notification envoyée - Status: " . $statusCode);
  213. error_log("✅ [WebSocket] Réponse: " . $body);
  214. if (isset($bodyData['clientsNotified'])) {
  215. $clientsNotified = $bodyData['clientsNotified'];
  216. error_log("📊 [WebSocket] Clients notifiés: " . $clientsNotified);
  217. if ($clientsNotified === 0) {
  218. error_log("⚠️ [WebSocket] ATTENTION: Aucun client n'a reçu le message !");
  219. error_log(" Raisons possibles:");
  220. error_log(" 1. Aucun navigateur connecté à cette conversation");
  221. error_log(" 2. Les clients ne sont pas dans la bonne room");
  222. error_log(" 3. Les clients n'ont pas appelé joinGroup()");
  223. }
  224. }
  225. error_log("═══════════════════════════════════════════════════════");
  226. } catch (\GuzzleHttp\Exception\ConnectException $e) {
  227. error_log("╔═══════════════════════════════════════════════════════╗");
  228. error_log("║ ❌ ERREUR DE CONNEXION WEBSOCKET ║");
  229. error_log("╚═══════════════════════════════════════════════════════╝");
  230. error_log("❌ [WebSocket] Impossible de se connecter au serveur");
  231. error_log(" URL tentée: " . ($websocketUrl ?? 'undefined'));
  232. error_log(" Message: " . $e->getMessage());
  233. error_log(" ⚠️ Le serveur WebSocket est-il démarré ?");
  234. error_log(" Commande: pm2 status");
  235. error_log("═══════════════════════════════════════════════════════");
  236. } catch (\GuzzleHttp\Exception\RequestException $e) {
  237. error_log("╔═══════════════════════════════════════════════════════╗");
  238. error_log("║ ❌ ERREUR REQUÊTE WEBSOCKET ║");
  239. error_log("╚═══════════════════════════════════════════════════════╝");
  240. error_log("❌ [WebSocket] Erreur lors de la requête");
  241. error_log(" Message: " . $e->getMessage());
  242. if ($e->hasResponse()) {
  243. error_log(" Status: " . $e->getResponse()->getStatusCode());
  244. error_log(" Body: " . $e->getResponse()->getBody()->getContents());
  245. }
  246. error_log("═══════════════════════════════════════════════════════");
  247. } catch (\Exception $e) {
  248. error_log("╔═══════════════════════════════════════════════════════╗");
  249. error_log("║ ❌ ERREUR GÉNÉRALE WEBSOCKET ║");
  250. error_log("╚═══════════════════════════════════════════════════════╝");
  251. error_log("❌ [WebSocket] Erreur: " . $e->getMessage());
  252. error_log(" Type: " . get_class($e));
  253. error_log(" File: " . $e->getFile() . ":" . $e->getLine());
  254. error_log("⚠️ Le message a été enregistré mais la notification temps réel n'a pas pu être envoyée");
  255. error_log("═══════════════════════════════════════════════════════");
  256. }
  257. return new JsonResponse([
  258. 'id' => (string)$message->getId(),
  259. 'message' => $message->getMessage(),
  260. 'isOwn' => true,
  261. 'userId' => $user->getId(),
  262. 'userName' => $fullname,
  263. 'avatar' => $avatar,
  264. 'createdAt' => $message->getCreatedAt() ? $message->getCreatedAt()->format('c') : null,
  265. ], Response::HTTP_CREATED);
  266. }
  267. /**
  268. * Récupérer les nouveaux messages depuis un certain ID (polling)
  269. */
  270. public function getNewMessages(Request $request, int $id): JsonResponse
  271. {
  272. $user = $this->getUser();
  273. $conversation = $this->em->getRepository(Groups::class)->find($id);
  274. if (!$conversation) {
  275. return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  276. }
  277. if (!$conversation->hasUser($user)) {
  278. return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  279. }
  280. $lastMessageId = $request->query->get('lastMessageId', 0);
  281. $messages = $this->em->getRepository(Messages::class)->findAfterMessage($conversation, $lastMessageId);
  282. $array = [];
  283. foreach ($messages as $message) {
  284. $isOwn = $message->getUser()->getId() === $user->getId();
  285. $fullname = $message->getUser()->getName()." ".$message->getUser()->getLastname();
  286. $avatar = $this->getDefaultAvatar();
  287. if ($message->getUser()->getImage()) {
  288. $avatar = $message->getUser()->getImageBase64();
  289. }
  290. $array[] = [
  291. "id" => (string)$message->getId(),
  292. "message" => (string)$message->getMessage(),
  293. "isOwn" => $isOwn,
  294. "userId" => $message->getUser()->getId(),
  295. "userName" => $fullname,
  296. "avatar" => $avatar,
  297. "createdAt" => $message->getCreatedAt() ? $message->getCreatedAt()->format('c') : null,
  298. ];
  299. }
  300. return new JsonResponse($array);
  301. }
  302. /**
  303. * Rechercher dans les conversations
  304. */
  305. public function search(Request $request): JsonResponse
  306. {
  307. $user = $this->getUser();
  308. $searchTerm = $request->query->get('q', '');
  309. if (empty(trim($searchTerm))) {
  310. return new JsonResponse(['error' => 'Terme de recherche requis'], Response::HTTP_BAD_REQUEST);
  311. }
  312. $conversations = $this->em->getRepository(Groups::class)->searchConversations($user, $searchTerm);
  313. $array = [];
  314. foreach ($conversations as $conversation) {
  315. $otherUser = $conversation->getOtherUser($user);
  316. if (!$otherUser) {
  317. continue;
  318. }
  319. $countMessages = $this->em->getRepository(Messages::class)->countByConversation($conversation);
  320. $fullname = $otherUser->getName()." ".$otherUser->getLastname();
  321. $avatar = $this->getDefaultAvatar();
  322. if ($otherUser->getImage()) {
  323. $avatar = $otherUser->getImageBase64();
  324. }
  325. $array[] = [
  326. "id" => (string)$conversation->getId(),
  327. "conversationHash" => $conversation->getConversationHash(),
  328. "title" => $conversation->getTitle() ?: 'Conversation avec ' . $fullname,
  329. "fullname" => $fullname,
  330. "userId" => $otherUser->getId(),
  331. "avatar" => $avatar,
  332. "count_messages" => $countMessages,
  333. "lastmessage" => $conversation->getLastMessageContent() ?? '',
  334. "lastMessageAt" => $conversation->getLastMessageAt() ? $conversation->getLastMessageAt()->format('c') : null,
  335. ];
  336. }
  337. return new JsonResponse($array);
  338. }
  339. /**
  340. * Supprimer une conversation (soft delete ou hard delete selon votre besoin)
  341. */
  342. public function deleteConversation(int $id): JsonResponse
  343. {
  344. $user = $this->getUser();
  345. $conversation = $this->em->getRepository(Groups::class)->find($id);
  346. if (!$conversation) {
  347. return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  348. }
  349. if (!$conversation->hasUser($user)) {
  350. return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  351. }
  352. return new JsonResponse(['message' => 'Conversation supprimée avec succès']);
  353. }
  354. /**
  355. * Démarrer une conversation
  356. */
  357. public function startConversation(Request $request, int $userId): JsonResponse
  358. {
  359. $user = $this->getUser();
  360. if (!$user) {
  361. return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  362. }
  363. $otherUser = $this->em->getRepository(Users::class)->find($userId);
  364. if (!$otherUser) {
  365. return new JsonResponse(['error' => 'Utilisateur non trouvé'], Response::HTTP_NOT_FOUND);
  366. }
  367. if ($user->getId() === $otherUser->getId()) {
  368. return new JsonResponse(['error' => 'Impossible de démarrer une conversation avec soi-même'], Response::HTTP_BAD_REQUEST);
  369. }
  370. $conversation = $this->em->getRepository(Groups::class)->findOrCreateConversation($user, $otherUser);
  371. $fullname = $otherUser->getName() . " " . $otherUser->getLastname();
  372. return new JsonResponse([
  373. 'id' => (string)$conversation->getId(),
  374. 'conversationHash' => $conversation->getConversationHash(),
  375. 'fullname' => $fullname,
  376. 'userId' => $otherUser->getId(),
  377. ], Response::HTTP_CREATED);
  378. }
  379. // ========================================
  380. // NOUVELLES MÉTHODES — V2 avec lu/non lu
  381. // ========================================
  382. /**
  383. * Liste des conversations récentes V2 — avec lastMessageSenderId et unread_count
  384. * Utilise le vrai compteur de messages non lus (basé sur readAt)
  385. */
  386. public function recentsV2(Request $request): JsonResponse
  387. {
  388. $user = $this->getUser();
  389. if (!$user) {
  390. return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  391. }
  392. $limit = $request->query->get('limit', 20);
  393. $conversations = $this->em->getRepository(Groups::class)->recents($user, $limit);
  394. $messagesRepo = $this->em->getRepository(Messages::class);
  395. $array = [];
  396. foreach ($conversations as $conversation) {
  397. $otherUser = $conversation->getOtherUser($user);
  398. if (!$otherUser) {
  399. continue;
  400. }
  401. // Compteur de messages NON LUS (messages de l'autre que je n'ai pas lus)
  402. $unreadCount = $messagesRepo->countUnreadByConversation($conversation, $user);
  403. $fullname = $otherUser->getName() . " " . $otherUser->getLastname();
  404. $avatar = $this->getDefaultAvatar();
  405. if ($otherUser->getImage()) {
  406. $avatar = $otherUser->getImageBase64();
  407. }
  408. $array[] = [
  409. "id" => (string)$conversation->getId(),
  410. "conversationHash" => $conversation->getConversationHash(),
  411. "title" => $conversation->getTitle() ?: 'Conversation avec ' . $fullname,
  412. "fullname" => $fullname,
  413. "userId" => $otherUser->getId(),
  414. "userEmail" => $otherUser->getEmail(),
  415. "avatar" => $avatar,
  416. "count_messages" => $unreadCount, // ← maintenant = non lus uniquement
  417. "lastMessageSenderId" => $conversation->getLastMessageSenderId(), // ← NOUVEAU
  418. "open" => false,
  419. "lastmessage" => $conversation->getLastMessageContent() ?? '',
  420. "lastMessageAt" => $conversation->getLastMessageAt() ? $conversation->getLastMessageAt()->format('c') : null,
  421. "createdAt" => $conversation->getCreatedAt() ? $conversation->getCreatedAt()->format('c') : null,
  422. ];
  423. }
  424. return new JsonResponse($array);
  425. }
  426. /**
  427. * Marquer tous les messages d'une conversation comme lus
  428. * Appelé quand l'utilisateur ouvre une conversation
  429. */
  430. public function markAsRead(int $id): JsonResponse
  431. {
  432. $user = $this->getUser();
  433. if (!$user) {
  434. return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  435. }
  436. $conversation = $this->em->getRepository(Groups::class)->find($id);
  437. if (!$conversation) {
  438. return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  439. }
  440. if (!$conversation->hasUser($user)) {
  441. return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  442. }
  443. // Marquer comme lus tous les messages de l'autre utilisateur
  444. $markedCount = $this->em->getRepository(Messages::class)
  445. ->markAsReadByConversation($conversation, $user);
  446. error_log("✅ [MarkAsRead] Conv {$id}{$markedCount} messages marqués comme lus pour user {$user->getId()}");
  447. return new JsonResponse([
  448. 'success' => true,
  449. 'markedCount' => $markedCount,
  450. ]);
  451. }
  452. /**
  453. * Obtenir le nombre total de messages non lus (pour le badge de l'onglet Messages)
  454. */
  455. public function totalUnread(): JsonResponse
  456. {
  457. $user = $this->getUser();
  458. if (!$user) {
  459. return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  460. }
  461. $totalUnread = $this->em->getRepository(Messages::class)
  462. ->countTotalUnreadForUser($user);
  463. return new JsonResponse([
  464. 'totalUnread' => $totalUnread,
  465. ]);
  466. }
  467. // ========================================
  468. // HELPERS PRIVÉS
  469. // ========================================
  470. private function cleanMessageNotification($texte, $longueur = 20) {
  471. if (mb_strlen($texte) <= $longueur) {
  472. return $texte;
  473. }
  474. return mb_substr($texte, 0, $longueur) . '...';
  475. }
  476. /**
  477. * Avatar SVG par défaut
  478. */
  479. private function getDefaultAvatar(): string
  480. {
  481. return "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNjY3ZWVhO3N0b3Atb3BhY2l0eToxIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiM3NjRiYTI7c3RvcC1vcGFjaXR5OjEiIC8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Y2lyY2xlIGN4PSIxMDAiIGN5PSIxMDAiIHI9IjEwMCIgZmlsbD0idXJsKCNncmFkKSIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCwgMTAwKSI+CiAgICA8Y2lyY2xlIGN4PSIwIiBjeT0iLTE1IiByPSIzNSIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgICA8cGF0aCBkPSJNIC01MCA2NSBRIC01MCAyNSAtMzUgMTAgUSAtMTUgLTUgMCAtNSBRIDE1IC01IDM1IDEwIFEgNTAgMjUgNTAgNjUgWiIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgPC9nPgo8L3N2Zz4K";
  482. }
  483. }