Skip to content

Commit 9c040cc

Browse files
committed
feat: Add map and granular instant notif preferences
1 parent 6c72a42 commit 9c040cc

11 files changed

Lines changed: 419 additions & 68 deletions

File tree

website/app-templates/smarty/dialog/user_update_email.tpl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,30 @@
2424
</label>
2525
</div>
2626

27+
<div id="instantNotificationsOptions" class="alert alert-info {if !$instant_notifications}hidden{/if}">
28+
<strong>{t}Instant notification types:{/t}</strong>
29+
<div class="checkbox">
30+
<label>
31+
<input type="checkbox" name="instant_notifications_moves_own_gk" class="instant-notif-checkbox" {if $instant_notifications_moves_own_gk}checked{/if}>{t}Moves of my GeoKrety{/t}
32+
</label>
33+
</div>
34+
<div class="checkbox">
35+
<label>
36+
<input type="checkbox" name="instant_notifications_moves_watched_gk" class="instant-notif-checkbox" {if $instant_notifications_moves_watched_gk}checked{/if}>{t}Moves of GeoKrety I watch{/t}
37+
</label>
38+
</div>
39+
<div class="checkbox">
40+
<label>
41+
<input type="checkbox" name="instant_notifications_moves_around_home" class="instant-notif-checkbox" {if $instant_notifications_moves_around_home}checked{/if}>{t}Moves around my home location{/t}
42+
</label>
43+
</div>
44+
<div class="checkbox">
45+
<label>
46+
<input type="checkbox" name="instant_notifications_move_comments" class="instant-notif-checkbox" {if $instant_notifications_move_comments}checked{/if}>{t}Comments on moves{/t}
47+
</label>
48+
</div>
49+
</div>
50+
2751
<hr />
2852
<p>
2953
<strong>{t}The main purpose of collecting email is to permit password recovery.{/t}</strong>

website/app-templates/smarty/emails/instant-move-notification.tpl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
</tbody>
5353
</table>
5454

55+
{if isset($notification_home_map_cid)}{* Map for moves around home notifications *}
56+
<div class="s-3"></div>
57+
<a href="{'geokrety_map'|alias}" title="{t}Open interactive map{/t}"><img src="cid:{$notification_home_map_cid}" alt="{t}Move location map{/t}" class="img-fluid max-w-150 align-center text-center"></a>
58+
{/if}
59+
5560
{if $comments}
5661
<table class="table table-striped thead-default table-bordered">
5762
<thead>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
$("body").on("change", "#instantNotificationsCheckbox", function(event) {
2+
const $optionsDiv = $("#instantNotificationsOptions");
3+
const $checkboxes = $(".instant-notif-checkbox");
4+
console.log("Instant notifications checkbox changed:", $(this).is(":checked"));
5+
if ($(this).is(":checked")) {
6+
$optionsDiv.removeClass("hidden");
7+
// Check all granular checkboxes when enabling instant notifications
8+
$checkboxes.prop("checked", true);
9+
} else {
10+
$optionsDiv.addClass("hidden");
11+
}
12+
});
13+
14+
// Initialize on page load
15+
$(document).ready(function() {
16+
$("#instantNotificationsCheckbox").trigger("change");
17+
});

website/app-templates/smarty/pages/user_details.tpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
// Bind modal
6565
{include 'js/dialogs/dialog_user_details.tpl.js'}
66+
{include 'js/dialogs/dialog_user_update_email.tpl.js'}
6667
{include 'js/dialogs/dialog_contact_user.tpl.js'}
6768
{include 'js/dialogs/dialog_picture_actions.tpl.js'}
6869
{include 'js/dialogs/dialog_oauth_disconnect.tpl.js'}

website/app/GeoKrety/Controller/Cli/DailyDigest.php

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
use GeoKrety\Model\MoveComment;
1111
use GeoKrety\Model\News;
1212
use GeoKrety\Model\User;
13-
use GeoKrety\Service\File;
1413
use GeoKrety\Service\Smarty;
14+
use GeoKrety\Service\StaticMapImage;
1515
use GeoKrety\Service\UserSettings;
1616
use PHPMailer\PHPMailer\Exception;
1717
use Sugar\Event;
@@ -269,52 +269,12 @@ private function load_near_home_dropped_geokrety() {
269269
SQL;
270270
$this->console_writer->print([$this->user->id, $this->user->username, 'load dropped - geojson']);
271271
$result = \Base::instance()->get('DB')->exec($sql, [$this->user->id, $this->since->format(GK_DB_DATETIME_FORMAT), Geokret::GEOKRETY_PRESENT_IN_CACHE]);
272-
$geojson = $result[0]['geojson'];
273-
274-
$home = <<<'GEOJSON'
275-
{
276-
"type": "Feature",
277-
"geometry": {
278-
"type": "Point",
279-
"coordinates": [ %s, %s]
280-
},
281-
"markerIconOptions": {
282-
"iconUrl": "%s/home48.png",
283-
"iconAnchor": [24, 24]
284-
}
285-
}
286-
GEOJSON;
287-
$home = sprintf($home, $this->user->home_longitude, $this->user->home_latitude, GK_CDN_ICONS_URL);
288-
289-
$geojson = json_decode($geojson);
290-
array_unshift($geojson->features, json_decode($home, true));
291-
292-
$img_url_params = http_build_query([
293-
// 'center' => sprintf('%s,%s', $this->user->home_longitude, $this->user->home_latitude),
294-
'arrows' => true,
295-
'geojson' => json_encode($geojson),
296-
'width' => 640,
297-
'height' => 480,
298-
'oxipng' => true,
299-
'maxZoom' => 13,
300-
// 'zoom' => 11,
301-
'markerIconOptions' => sprintf('{"iconUrl": "%s/pins/green.png", iconAnchor: [6, 20]}', GK_CDN_ICONS_URL),
302-
]);
303-
$this->console_writer->print([$this->user->id, $this->user->username, 'load dropped - image']);
304-
try {
305-
$fp = fopen('php://memory', 'w');
306-
File::download(sprintf('%s?%s', GK_OSM_STATIC_MAPS_URI, $img_url_params), $fp);
307-
rewind($fp);
308-
$img_string = stream_get_contents($fp);
309-
fclose($fp);
310-
} catch (\Exception $e) {
311-
echo $this->console_writer->sprintf('E: Download static maps image failed: %s', $e->getMessage());
272+
$positions = $result;
312273

313-
return;
274+
$this->console_writer->print([$this->user->id, $this->user->username, 'load dropped - image']);
275+
$imgCid = 'GK_NEAR_HOME_IMG';
276+
if (StaticMapImage::generateHomeMapWithMarkers($this->email, $this->user, $positions, $imgCid)) {
277+
Smarty::assign('gk_near_home_img', $imgCid);
314278
}
315-
316-
$img_cid = 'GK_NEAR_HOME_IMG';
317-
Smarty::assign('gk_near_home_img', $img_cid);
318-
$this->email->addStringEmbeddedImage($img_string, $img_cid, 'gk_near_home.png');
319279
}
320280
}

website/app/GeoKrety/Controller/Pages/UserUpdateEmail.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public function get(\Base $f3) {
2020
}
2121
Smarty::assign('daily_digest', UserSettings::getForCurrentUser('DAILY_DIGEST'));
2222
Smarty::assign('instant_notifications', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS'));
23+
Smarty::assign('instant_notifications_moves_own_gk', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_OWN_GK'));
24+
Smarty::assign('instant_notifications_moves_watched_gk', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_WATCHED_GK'));
25+
Smarty::assign('instant_notifications_moves_around_home', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_AROUND_HOME'));
26+
Smarty::assign('instant_notifications_move_comments', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVE_COMMENTS'));
2327
Smarty::render('extends:full_screen_modal.tpl|dialog/user_update_email.tpl');
2428
}
2529

@@ -30,6 +34,10 @@ public function get_ajax(\Base $f3) {
3034
}
3135
Smarty::assign('daily_digest', UserSettings::getForCurrentUser('DAILY_DIGEST'));
3236
Smarty::assign('instant_notifications', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS'));
37+
Smarty::assign('instant_notifications_moves_own_gk', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_OWN_GK'));
38+
Smarty::assign('instant_notifications_moves_watched_gk', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_WATCHED_GK'));
39+
Smarty::assign('instant_notifications_moves_around_home', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_AROUND_HOME'));
40+
Smarty::assign('instant_notifications_move_comments', UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVE_COMMENTS'));
3341
Smarty::render('extends:base_modal.tpl|dialog/user_update_email.tpl');
3442
}
3543

@@ -41,6 +49,18 @@ public function post(\Base $f3) {
4149
// Get values from POST
4250
$daily_digest = filter_var($f3->get('POST.daily_digest'), FILTER_VALIDATE_BOOLEAN);
4351
$instant_notifications = filter_var($f3->get('POST.instant_notifications'), FILTER_VALIDATE_BOOLEAN);
52+
$instant_notifications_moves_own_gk = filter_var($f3->get('POST.instant_notifications_moves_own_gk'), FILTER_VALIDATE_BOOLEAN);
53+
$instant_notifications_moves_watched_gk = filter_var($f3->get('POST.instant_notifications_moves_watched_gk'), FILTER_VALIDATE_BOOLEAN);
54+
$instant_notifications_moves_around_home = filter_var($f3->get('POST.instant_notifications_moves_around_home'), FILTER_VALIDATE_BOOLEAN);
55+
$instant_notifications_move_comments = filter_var($f3->get('POST.instant_notifications_move_comments'), FILTER_VALIDATE_BOOLEAN);
56+
57+
// If instant_notifications is enabled, default all granular settings to true if not explicitly set
58+
if ($instant_notifications) {
59+
$instant_notifications_moves_own_gk = $instant_notifications_moves_own_gk ?? true;
60+
$instant_notifications_moves_watched_gk = $instant_notifications_moves_watched_gk ?? true;
61+
$instant_notifications_moves_around_home = $instant_notifications_moves_around_home ?? true;
62+
$instant_notifications_move_comments = $instant_notifications_move_comments ?? true;
63+
}
4464

4565
// Save user preferences using UserSettings service
4666
$userSettings = UserSettings::instance();
@@ -56,6 +76,26 @@ public function post(\Base $f3) {
5676
$changed = true;
5777
}
5878

79+
if (UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_OWN_GK') !== $instant_notifications_moves_own_gk) {
80+
$userSettings->put($user, 'INSTANT_NOTIFICATIONS_MOVES_OWN_GK', $instant_notifications_moves_own_gk ? 'true' : 'false');
81+
$changed = true;
82+
}
83+
84+
if (UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_WATCHED_GK') !== $instant_notifications_moves_watched_gk) {
85+
$userSettings->put($user, 'INSTANT_NOTIFICATIONS_MOVES_WATCHED_GK', $instant_notifications_moves_watched_gk ? 'true' : 'false');
86+
$changed = true;
87+
}
88+
89+
if (UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVES_AROUND_HOME') !== $instant_notifications_moves_around_home) {
90+
$userSettings->put($user, 'INSTANT_NOTIFICATIONS_MOVES_AROUND_HOME', $instant_notifications_moves_around_home ? 'true' : 'false');
91+
$changed = true;
92+
}
93+
94+
if (UserSettings::getForCurrentUser('INSTANT_NOTIFICATIONS_MOVE_COMMENTS') !== $instant_notifications_move_comments) {
95+
$userSettings->put($user, 'INSTANT_NOTIFICATIONS_MOVE_COMMENTS', $instant_notifications_move_comments ? 'true' : 'false');
96+
$changed = true;
97+
}
98+
5999
if ($changed) {
60100
\Flash::instance()->addMessage(_('Your email preferences were saved.'), 'success');
61101
}

website/app/GeoKrety/Model/Move.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ public function get_move_type($value): LogType {
158158
return new LogType($value);
159159
}
160160

161-
public function get_lat($value) {
161+
public function get_lat($value): mixed {
162162
return $value ? number_format(floatval($value), 5, '.', '') : $value;
163163
}
164164

165-
public function get_lon($value) {
165+
public function get_lon($value): mixed {
166166
return $value ? number_format(floatval($value), 5, '.', '') : $value;
167167
}
168168

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
namespace GeoKrety\Service;
4+
5+
use GeoKrety\Email\BasePHPMailer;
6+
use GeoKrety\Model\User;
7+
8+
class StaticMapImage {
9+
private const HOME_ICON_SIZE = 48;
10+
private const GK_ICON_SIZE = 16;
11+
12+
/**
13+
* Generate and embed a static map image showing GeoKrets dropped near user's home.
14+
*
15+
* @param BasePHPMailer $email Email instance to embed image in
16+
* @param User $user User object with home coordinates
17+
* @param array $positions Array of position geometries (from query result)
18+
* @param string $imageId Content ID for embedding (default: GK_NEAR_HOME_IMG)
19+
*
20+
* @return bool true if image was successfully generated and embedded, false otherwise
21+
*
22+
* @throws \Exception If file download or image handling fails
23+
*/
24+
public static function generateHomeMapWithMarkers(
25+
BasePHPMailer $email,
26+
User $user,
27+
array $positions,
28+
string $imageId = 'GK_NEAR_HOME_IMG',
29+
): bool {
30+
if (!$user->hasHomeCoordinates()) {
31+
return false;
32+
}
33+
34+
try {
35+
// Build GeoJSON for the map
36+
$geojson = self::buildGeoJSON($positions, $user);
37+
38+
// Build URL parameters for static maps service
39+
$imgUrlParams = http_build_query([
40+
'arrows' => true,
41+
'geojson' => json_encode($geojson),
42+
'width' => 640,
43+
'height' => 480,
44+
'oxipng' => true,
45+
'maxZoom' => 13,
46+
'markerIconOptions' => sprintf(
47+
'{"iconUrl": "%s/pins/green.png", iconAnchor: [6, 20]}',
48+
GK_CDN_ICONS_URL
49+
),
50+
]);
51+
52+
$mapUrl = sprintf('%s?%s', GK_OSM_STATIC_MAPS_URI, $imgUrlParams);
53+
54+
// Download the image
55+
$fp = fopen('php://memory', 'w');
56+
File::download($mapUrl, $fp);
57+
rewind($fp);
58+
$imgString = stream_get_contents($fp);
59+
fclose($fp);
60+
61+
// Embed image in email
62+
$email->addStringEmbeddedImage($imgString, $imageId, 'gk_near_home.png');
63+
64+
return true;
65+
} catch (\Exception $e) {
66+
return false;
67+
}
68+
}
69+
70+
/**
71+
* Build GeoJSON FeatureCollection from positions with home location marker.
72+
*
73+
* @param array $queryResult Array from database query containing 'geojson' field
74+
* @param User $user User object with home coordinates
75+
*
76+
* @return object GeoJSON FeatureCollection
77+
*/
78+
private static function buildGeoJSON(array $queryResult, User $user): object {
79+
// Build home marker feature
80+
$homeFeature = self::buildHomeMarkerFeature($user);
81+
82+
// Extract GeoJSON from query result
83+
if (!empty($queryResult) && isset($queryResult[0]['geojson'])) {
84+
$geojson = json_decode($queryResult[0]['geojson']);
85+
} else {
86+
$geojson = (object) [
87+
'type' => 'FeatureCollection',
88+
'features' => [],
89+
];
90+
}
91+
92+
// Ensure features array exists
93+
if (!isset($geojson->features) || !is_array($geojson->features)) {
94+
$geojson->features = [];
95+
}
96+
97+
// Add home marker at the beginning
98+
array_unshift($geojson->features, $homeFeature);
99+
100+
return $geojson;
101+
}
102+
103+
/**
104+
* Build a GeoJSON feature for home location marker.
105+
*
106+
* @param User $user User with home coordinates
107+
*
108+
* @return array GeoJSON Feature object
109+
*/
110+
private static function buildHomeMarkerFeature(User $user): array {
111+
return [
112+
'type' => 'Feature',
113+
'geometry' => [
114+
'type' => 'Point',
115+
'coordinates' => [(float) $user->home_longitude, (float) $user->home_latitude],
116+
],
117+
'markerIconOptions' => [
118+
'iconUrl' => GK_CDN_ICONS_URL.'/home'.self::HOME_ICON_SIZE.'.png',
119+
'iconAnchor' => [24, 24],
120+
],
121+
];
122+
}
123+
}

website/app/shutdown.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ function shutdown_force_send_response_to_client(Base $f3) {
1616
}
1717

1818
function shutdown_piwik(Base $f3) {
19+
// Skip in worker/CLI context where there's no current user/session
20+
if (!$f3->exists('SESSION.CURRENT_USER')) {
21+
return;
22+
}
23+
1924
$ip = $f3->get('IP');
2025
$ip_excluded = false;
2126
foreach (GK_SYSTEM_PATH_ALLOWED_IPS as $range) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Phinx\Migration\AbstractMigration;
6+
7+
final class AddGranularInstantNotifications extends AbstractMigration {
8+
public function up(): void {
9+
// 1. Add granular instant notification settings parameters
10+
$this->execute("
11+
INSERT INTO geokrety.gk_users_settings_parameters (name, type, \"default\", description, created_on_datetime, updated_on_datetime)
12+
VALUES
13+
('INSTANT_NOTIFICATIONS_MOVES_OWN_GK', 'bool', 'true', 'Receive instant notifications for moves of my own GeoKrety', NOW(), NOW()),
14+
('INSTANT_NOTIFICATIONS_MOVES_WATCHED_GK', 'bool', 'true', 'Receive instant notifications for moves of GeoKrety I watch', NOW(), NOW()),
15+
('INSTANT_NOTIFICATIONS_MOVES_AROUND_HOME', 'bool', 'true', 'Receive instant notifications for moves around my home location', NOW(), NOW()),
16+
('INSTANT_NOTIFICATIONS_MOVE_COMMENTS', 'bool', 'true', 'Receive instant notifications for comments on moves', NOW(), NOW());
17+
");
18+
}
19+
20+
public function down(): void {
21+
// 1. Remove granular instant notification settings parameters
22+
$this->execute("
23+
DELETE FROM geokrety.gk_users_settings_parameters
24+
WHERE name IN (
25+
'INSTANT_NOTIFICATIONS_MOVES_OWN_GK',
26+
'INSTANT_NOTIFICATIONS_MOVES_WATCHED_GK',
27+
'INSTANT_NOTIFICATIONS_MOVES_AROUND_HOME',
28+
'INSTANT_NOTIFICATIONS_MOVE_COMMENTS'
29+
);
30+
");
31+
32+
// 2. Clean up any user settings for these parameters
33+
$this->execute("
34+
DELETE FROM geokrety.gk_users_settings
35+
WHERE name IN (
36+
'INSTANT_NOTIFICATIONS_MOVES_OWN_GK',
37+
'INSTANT_NOTIFICATIONS_MOVES_WATCHED_GK',
38+
'INSTANT_NOTIFICATIONS_MOVES_AROUND_HOME',
39+
'INSTANT_NOTIFICATIONS_MOVE_COMMENTS'
40+
);
41+
");
42+
}
43+
}

0 commit comments

Comments
 (0)