أساسيات JavaScript

واجهة الإشعارات والأذونات

50 دقيقة الدرس 57 من 60

مقدمة في واجهة الإشعارات

تتيح واجهة الإشعارات (Notification API) لتطبيقات الويب إرسال إشعارات للمستخدمين خارج سياق تبويب المتصفح. تظهر هذه الإشعارات على مستوى النظام -- في مركز إشعارات نظام التشغيل على أجهزة سطح المكتب أو كتنبيهات دفع على الأجهزة المحمولة. وهي ضرورية لإبقاء المستخدمين على اطلاع بالأحداث المهمة مثل الرسائل الجديدة وتذكيرات المهام والتحديثات الفورية واكتمال العمليات في الخلفية حتى عندما لا يكون المستخدم ينظر بنشاط إلى تطبيقك.

واجهة الإشعارات هي ميزة أصلية في المتصفح تتطلب إذنًا صريحًا من المستخدم قبل عرض أي إشعارات. نموذج الأذونات هذا جزء حيوي من بنية أمان منصة الويب، يضمن عدم قدرة المواقع على إغراق المستخدمين بتنبيهات غير مرغوبة. فهم كيفية طلب الأذونات والتحقق منها والتعامل معها بشكل صحيح لا يقل أهمية عن معرفة كيفية إنشاء الإشعارات وعرضها.

ملاحظة: تعمل واجهة الإشعارات في معظم المتصفحات الحديثة بما في ذلك Chrome وFirefox وEdge وSafari. ومع ذلك، فإن Safari على iOS لديه دعم محدود ويتطلب استخدام عمال الخدمة لإشعارات الدفع عبر الويب. تحقق دائمًا من توافق المتصفح ووفر سلوكًا بديلاً للبيئات التي لا تدعم الإشعارات.

التحقق من دعم الإشعارات

قبل محاولة استخدام واجهة الإشعارات، يجب عليك دائمًا التحقق من أن المتصفح يدعمها. كائن Notification متاح في النطاق العام window في المتصفحات الداعمة.

مثال: التحقق من دعم واجهة الإشعارات

function isNotificationSupported() {
    if (!("Notification" in window)) {
        console.log("هذا المتصفح لا يدعم الإشعارات.");
        return false;
    }
    console.log("الإشعارات مدعومة!");
    return true;
}

// الاستخدام
if (isNotificationSupported()) {
    // المتابعة مع منطق الإشعارات
    console.log("الإذن الحالي:", Notification.permission);
}

فهم حالات الأذونات

تستخدم واجهة الإشعارات نموذج أذونات من ثلاث حالات يحدد ما إذا كان تطبيقك يمكنه عرض الإشعارات. هذه الحالات متاحة عبر Notification.permission وهي أساسية لبناء تجربة إشعارات جيدة.

  • default -- لم يتخذ المستخدم قرارًا بعد. يمكنك طلب الإذن وسيعرض المتصفح مطالبة تسأل المستخدم السماح أو حظر الإشعارات.
  • granted -- سمح المستخدم صراحةً بالإشعارات. يمكنك إنشاء وعرض الإشعارات بحرية.
  • denied -- حظر المستخدم صراحةً الإشعارات. لا يمكنك عرض الإشعارات ولا يمكنك إعادة مطالبة المستخدم. يجب عليهم تغيير هذا الإعداد يدويًا في تفضيلات المتصفح.

مثال: التحقق من حالة الإذن الحالية

function checkNotificationPermission() {
    switch (Notification.permission) {
        case "granted":
            console.log("تم منح الإذن. يمكن عرض الإشعارات.");
            return true;

        case "denied":
            console.log("تم رفض الإذن. يجب على المستخدم التمكين من إعدادات المتصفح.");
            return false;

        case "default":
            console.log("لم يُطلب الإذن بعد. سيتم مطالبة المستخدم.");
            return null;

        default:
            console.log("حالة إذن غير معروفة.");
            return false;
    }
}

const status = checkNotificationPermission();
مهم: بمجرد رفض المستخدم لإذن الإشعارات، لا يمكن لتطبيقك طلبه مرة أخرى برمجيًا. سيتجاهل المتصفح بصمت الاستدعاءات اللاحقة لـ Notification.requestPermission(). يجب على المستخدم الانتقال يدويًا إلى إعدادات المتصفح لإعادة تمكين الإشعارات لموقعك. صمم تجربة المستخدم الخاصة بك لشرح قيمة الإشعارات قبل طلب الإذن، وتعامل مع حالة الرفض بأناقة.

طلب الإذن

يُطلب الإذن باستخدام طريقة Notification.requestPermission(). تعيد هذه الطريقة وعدًا (Promise) يُحل بخيار المستخدم. أفضل الممارسات الحديثة هي استخدام صيغة Promise، رغم وجود صيغة قديمة تعتمد على دوال الاستدعاء للتوافق مع الإصدارات السابقة.

مثال: طلب إذن الإشعارات (قائم على Promise)

async function requestNotificationPermission() {
    if (!("Notification" in window)) {
        console.log("الإشعارات غير مدعومة.");
        return "unsupported";
    }

    // إذا كان ممنوحًا بالفعل، لا حاجة للسؤال مرة أخرى
    if (Notification.permission === "granted") {
        console.log("الإذن ممنوح بالفعل.");
        return "granted";
    }

    // إذا كان مرفوضًا، لا يمكن إعادة السؤال
    if (Notification.permission === "denied") {
        console.log("تم رفض الإذن سابقًا.");
        return "denied";
    }

    // طلب الإذن من المستخدم
    try {
        const permission = await Notification.requestPermission();
        console.log("استجاب المستخدم بـ:", permission);
        return permission;
    } catch (error) {
        console.error("خطأ في طلب الإذن:", error);
        return "error";
    }
}

// الاستخدام
requestNotificationPermission().then((result) => {
    if (result === "granted") {
        console.log("جاهز لإرسال الإشعارات!");
    }
});
نصيحة احترافية: لا تطلب إذن الإشعارات فورًا عند تحميل الصفحة. يُعتبر هذا تجربة مستخدم سيئة وستحظر العديد من المتصفحات هذه الطلبات تلقائيًا. بدلاً من ذلك، انتظر تفاعلاً ذا معنى من المستخدم -- مثل النقر على زر "تفعيل الإشعارات" -- واشرح لماذا ستكون الإشعارات مفيدة قبل طلب الإذن. هذا يزيد بشكل كبير من معدل القبول.

مُنشئ الإشعارات

بمجرد منح الإذن، تُنشئ الإشعارات باستخدام المُنشئ new Notification(title, options). الوسيط الأول هو عنوان الإشعار (سلسلة نصية مطلوبة)، والثاني هو كائن تكوين اختياري يتحكم في مظهر وسلوك الإشعار.

مثال: إنشاء إشعار بسيط

function showBasicNotification() {
    if (Notification.permission !== "granted") {
        console.log("لا يوجد إذن لعرض الإشعارات.");
        return;
    }

    const notification = new Notification("مرحبًا بالعالم!", {
        body: "هذا هو أول إشعار متصفح لك.",
        icon: "/images/notification-icon.png"
    });

    console.log("تم إنشاء الإشعار:", notification);
}

showBasicNotification();

خيارات الإشعارات بالتفصيل

يتحكم كائن الخيارات الذي يُمرر لمُنشئ Notification في كل جانب من مظهر وسلوك الإشعار. إليك شرحًا شاملاً لجميع الخيارات المتاحة.

مثال: خيارات الإشعارات الكاملة

function showDetailedNotification() {
    const options = {
        // المحتوى النصي
        body: "لديك 3 رسائل جديدة من فريقك.",

        // العناصر البصرية
        icon: "/images/app-icon-192.png",      // أيقونة صغيرة (عادة شعار التطبيق)
        badge: "/images/badge-icon-72.png",     // أيقونة أحادية اللون لشريط الحالة
        image: "/images/preview-large.jpg",     // معاينة صورة كبيرة

        // التجميع والاستبدال
        tag: "messages-group",                   // معرف المجموعة -- يستبدل إشعارات بنفس الوسم
        renotify: true,                          // التنبيه مرة أخرى حتى عند استبدال نفس الوسم

        // السلوك
        silent: false,                           // إذا كان true، بدون صوت أو اهتزاز
        requireInteraction: false,               // إذا كان true، يبقى حتى يتفاعل المستخدم

        // حمولة البيانات
        data: {
            url: "/messages",
            messageCount: 3,
            timestamp: Date.now()
        },

        // نمط الاهتزاز (الجوال): اهتزاز، توقف، اهتزاز
        vibrate: [200, 100, 200],

        // اتجاه النص
        dir: "rtl",                              // "ltr" أو "rtl" أو "auto"
        lang: "ar",                              // وسم اللغة

        // الإجراءات (تعمل فقط مع إشعارات عامل الخدمة)
        actions: [
            { action: "view", title: "عرض الرسائل", icon: "/images/view.png" },
            { action: "dismiss", title: "تجاهل", icon: "/images/dismiss.png" }
        ],

        // الطابع الزمني لوقت حدوث الحدث
        timestamp: Date.now() - 30000           // قبل 30 ثانية
    };

    const notification = new Notification("رسائل جديدة", options);
    return notification;
}
ملاحظة: خاصية tag مهمة بشكل خاص لإدارة الإشعارات المتعددة. عند إنشاء إشعار جديد بنفس الوسم كإشعار موجود، يستبدل الإشعار الجديد القديم بدلاً من تكديس إدخال جديد. عيّن renotify: true إذا كنت تريد أن يستمر الاستبدال في تنبيه المستخدم بالصوت والاهتزاز. بدون renotify، يحدث الاستبدال بصمت.

أحداث الإشعارات

تُطلق الإشعارات أربعة أحداث تتيح لك الاستجابة لتفاعل المستخدم. هذه الأحداث ضرورية لإنشاء تجربة إشعارات متجاوبة -- فتح الصفحات ذات الصلة عند النقر، وتسجيل التحليلات عند الإغلاق، والتعامل مع الأخطاء بأناقة.

مثال: التعامل مع جميع أحداث الإشعارات

function showInteractiveNotification() {
    const notification = new Notification("تذكير بالمهمة", {
        body: "موعد تسليم مشروعك بعد ساعة واحدة.",
        icon: "/images/task-icon.png",
        tag: "task-reminder",
        data: { taskId: 42, url: "/tasks/42" }
    });

    // يُطلق عند عرض الإشعار
    notification.onshow = function(event) {
        console.log("تم عرض الإشعار:", event);
        trackEvent("notification_shown", { tag: "task-reminder" });
    };

    // يُطلق عند نقر المستخدم على الإشعار
    notification.onclick = function(event) {
        event.preventDefault();
        console.log("تم النقر على الإشعار:", event);

        const url = event.target.data.url;
        window.open(url, "_blank");

        notification.close();
    };

    // يُطلق عند إغلاق الإشعار (من المستخدم أو برمجيًا)
    notification.onclose = function(event) {
        console.log("تم إغلاق الإشعار:", event);
        trackEvent("notification_closed", { tag: "task-reminder" });
    };

    // يُطلق عند حدوث خطأ
    notification.onerror = function(event) {
        console.error("خطأ في الإشعار:", event);
        showInAppFallback("تذكير بالمهمة", "موعد التسليم بعد ساعة واحدة.");
    };

    return notification;
}

function trackEvent(name, data) {
    console.log("حدث تحليلي:", name, data);
}

function showInAppFallback(title, message) {
    console.log("إشعار بديل:", title, "-", message);
}

إغلاق الإشعارات برمجيًا

يمكنك إغلاق إشعار في أي وقت باستخدام طريقة close(). هذا مفيد لرفض الإشعارات بعد أن أدت غرضها، أو لتنفيذ سلوك الإغلاق التلقائي مع مهلة زمنية.

مثال: إشعارات ذاتية الإغلاق

function showTimedNotification(title, body, duration = 5000) {
    const notification = new Notification(title, {
        body: body,
        icon: "/images/info-icon.png",
        tag: "timed-" + Date.now()
    });

    // إغلاق تلقائي بعد المدة المحددة
    const timer = setTimeout(() => {
        notification.close();
        console.log("تم إغلاق الإشعار تلقائيًا بعد", duration, "مللي ثانية");
    }, duration);

    // مسح المؤقت إذا أغلق المستخدم يدويًا أو نقر
    notification.onclose = function() {
        clearTimeout(timer);
    };

    notification.onclick = function() {
        clearTimeout(timer);
        notification.close();
    };

    return notification;
}

// عرض إشعار يُغلق تلقائيًا بعد 8 ثوانٍ
showTimedNotification(
    "اكتمل الرفع",
    "تم رفع ملفك بنجاح.",
    8000
);

واجهة الأذونات (Permissions API)

بينما تملك واجهة الإشعارات خاصية Notification.permission الخاصة بها، توفر واجهة الأذونات الأوسع طريقة موحدة للاستعلام عن الأذونات ومراقبتها لمختلف ميزات المتصفح بما في ذلك الإشعارات والموقع الجغرافي والكاميرا والميكروفون والمزيد. يتم الوصول إلى واجهة الأذونات عبر navigator.permissions.

مثال: الاستعلام عن إذن الإشعارات باستخدام واجهة الأذونات

async function queryNotificationPermission() {
    if (!navigator.permissions) {
        console.log("واجهة الأذونات غير مدعومة.");
        return null;
    }

    try {
        const permissionStatus = await navigator.permissions.query({
            name: "notifications"
        });

        console.log("حالة إذن الإشعارات:", permissionStatus.state);
        // state يمكن أن يكون: "granted" أو "denied" أو "prompt"

        return permissionStatus;
    } catch (error) {
        console.error("خطأ في الاستعلام عن الإذن:", error);
        return null;
    }
}

queryNotificationPermission();

مراقبة تغييرات الأذونات

واحدة من أقوى ميزات واجهة الأذونات هي القدرة على الاستماع لتغييرات الأذونات في الوقت الفعلي. يُطلق كائن PermissionStatus حدث change كلما عدّل المستخدم الإذن من خلال إعدادات المتصفح.

مثال: مراقبة تغييرات الأذونات

async function watchNotificationPermission() {
    if (!navigator.permissions) {
        console.log("واجهة الأذونات غير متاحة.");
        return;
    }

    try {
        const status = await navigator.permissions.query({
            name: "notifications"
        });

        console.log("الحالة الأولية:", status.state);

        // الاستماع للتغييرات
        status.addEventListener("change", function() {
            console.log("تغير الإذن إلى:", status.state);

            switch (status.state) {
                case "granted":
                    enableNotificationFeatures();
                    break;
                case "denied":
                    disableNotificationFeatures();
                    showPermissionDeniedMessage();
                    break;
                case "prompt":
                    showEnableNotificationsButton();
                    break;
            }
        });
    } catch (error) {
        console.error("فشل في مراقبة الأذونات:", error);
    }
}

function enableNotificationFeatures() {
    console.log("تمكين عناصر واجهة الإشعارات...");
}

function disableNotificationFeatures() {
    console.log("تعطيل عناصر واجهة الإشعارات...");
}

function showPermissionDeniedMessage() {
    console.log("عرض تعليمات لإعادة التمكين من إعدادات المتصفح...");
}

function showEnableNotificationsButton() {
    console.log("عرض زر 'تفعيل الإشعارات'...");
}

watchNotificationPermission();

الاستعلام عن أذونات متعددة

تدعم واجهة الأذونات الاستعلام عن ميزات متصفح متنوعة. يمكنك التحقق من أذونات متعددة لبناء منطق كشف ميزات شامل لتطبيقك.

مثال: الاستعلام عن أذونات متعددة

async function checkAllPermissions() {
    const permissionNames = [
        "notifications",
        "geolocation",
        "camera",
        "microphone"
    ];

    const results = {};

    for (const name of permissionNames) {
        try {
            const status = await navigator.permissions.query({ name });
            results[name] = status.state;
        } catch (error) {
            results[name] = "unsupported";
        }
    }

    console.log("حالات الأذونات:", results);
    return results;
}

// الاستخدام
checkAllPermissions().then((permissions) => {
    if (permissions.notifications === "granted") {
        console.log("الإشعارات مفعلة.");
    }
    if (permissions.geolocation === "prompt") {
        console.log("لم يُطلب إذن الموقع الجغرافي بعد.");
    }
});

بناء نظام إشعارات متكامل

في التطبيقات الحقيقية، تحتاج إلى نظام إشعارات قوي يتعامل مع إدارة الأذونات وإنشاء الإشعارات وتتبع الأحداث والسلوك البديل في وحدة واحدة متماسكة. إليك فئة مدير إشعارات شاملة تغلف كل هذه الوظائف.

مثال: مدير الإشعارات المتكامل

class NotificationManager {
    constructor(options = {}) {
        this.defaultIcon = options.icon || "/images/default-icon.png";
        this.defaultBadge = options.badge || "/images/default-badge.png";
        this.defaultDuration = options.duration || 0;
        this.activeNotifications = new Map();
        this.eventListeners = new Map();
        this.isSupported = "Notification" in window;
    }

    // التحقق من دعم الإشعارات
    get supported() {
        return this.isSupported;
    }

    // الحصول على حالة الإذن الحالية
    get permission() {
        if (!this.isSupported) return "unsupported";
        return Notification.permission;
    }

    // طلب الإذن من المستخدم
    async requestPermission() {
        if (!this.isSupported) {
            return "unsupported";
        }

        if (Notification.permission === "granted") {
            return "granted";
        }

        if (Notification.permission === "denied") {
            return "denied";
        }

        try {
            const result = await Notification.requestPermission();
            this.emit("permissionchange", result);
            return result;
        } catch (error) {
            console.error("فشل طلب الإذن:", error);
            return "error";
        }
    }

    // عرض إشعار
    show(title, options = {}) {
        if (!this.isSupported || Notification.permission !== "granted") {
            this.showFallback(title, options);
            return null;
        }

        const notificationOptions = {
            body: options.body || "",
            icon: options.icon || this.defaultIcon,
            badge: options.badge || this.defaultBadge,
            tag: options.tag || "default-" + Date.now(),
            data: options.data || {},
            silent: options.silent || false,
            renotify: options.renotify || false,
            requireInteraction: options.requireInteraction || false,
            ...options
        };

        try {
            const notification = new Notification(title, notificationOptions);
            const id = notificationOptions.tag;
            this.activeNotifications.set(id, notification);

            notification.onclick = (event) => {
                event.preventDefault();
                this.emit("click", { notification, event, data: notificationOptions.data });
                if (options.onClick) options.onClick(event, notificationOptions.data);
                notification.close();
            };

            notification.onclose = () => {
                this.activeNotifications.delete(id);
                this.emit("close", { id, data: notificationOptions.data });
                if (options.onClose) options.onClose(notificationOptions.data);
            };

            notification.onerror = (event) => {
                this.emit("error", { event, id });
                this.showFallback(title, options);
            };

            notification.onshow = () => {
                this.emit("show", { id, title });
            };

            const duration = options.duration || this.defaultDuration;
            if (duration > 0) {
                setTimeout(() => {
                    if (this.activeNotifications.has(id)) {
                        notification.close();
                    }
                }, duration);
            }

            return notification;
        } catch (error) {
            console.error("فشل إنشاء الإشعار:", error);
            this.showFallback(title, options);
            return null;
        }
    }

    // إغلاق إشعار محدد بالوسم
    close(tag) {
        const notification = this.activeNotifications.get(tag);
        if (notification) {
            notification.close();
            this.activeNotifications.delete(tag);
        }
    }

    // إغلاق جميع الإشعارات النشطة
    closeAll() {
        this.activeNotifications.forEach((notification) => {
            notification.close();
        });
        this.activeNotifications.clear();
    }

    // الحصول على عدد الإشعارات النشطة
    get activeCount() {
        return this.activeNotifications.size;
    }

    // البديل داخل التطبيق عندما لا تتوفر الإشعارات
    showFallback(title, options = {}) {
        const container = document.getElementById("notification-container");
        if (!container) return;

        const toast = document.createElement("div");
        toast.className = "in-app-notification";
        toast.innerHTML = "<strong>" + title + "</strong>"
            + (options.body ? "<p>" + options.body + "</p>" : "");

        container.appendChild(toast);

        setTimeout(() => {
            toast.classList.add("fade-out");
            setTimeout(() => toast.remove(), 300);
        }, options.duration || 5000);

        this.emit("fallback", { title, options });
    }

    // باعث أحداث بسيط
    on(event, callback) {
        if (!this.eventListeners.has(event)) {
            this.eventListeners.set(event, []);
        }
        this.eventListeners.get(event).push(callback);
    }

    off(event, callback) {
        if (!this.eventListeners.has(event)) return;
        const listeners = this.eventListeners.get(event);
        const index = listeners.indexOf(callback);
        if (index > -1) listeners.splice(index, 1);
    }

    emit(event, data) {
        if (!this.eventListeners.has(event)) return;
        this.eventListeners.get(event).forEach((cb) => cb(data));
    }
}

// الاستخدام
const notifier = new NotificationManager({
    icon: "/images/app-icon.png",
    duration: 10000
});

notifier.on("click", ({ data }) => {
    console.log("نقر المستخدم على الإشعار، البيانات:", data);
    if (data.url) window.open(data.url, "_blank");
});

notifier.on("permissionchange", (state) => {
    console.log("تغير الإذن:", state);
});

استخدام مدير الإشعارات

مع وجود مدير الإشعارات، يصبح إرسال الإشعارات مباشرًا ومتسقًا عبر تطبيقك بالكامل.

مثال: الاستخدام العملي لمدير الإشعارات

// تهيئة المدير
const notifier = new NotificationManager({
    icon: "/images/app-icon.png"
});

// طلب الإذن عند تفاعل المستخدم
document.getElementById("enable-btn").addEventListener("click", async () => {
    const permission = await notifier.requestPermission();

    if (permission === "granted") {
        notifier.show("تم تفعيل الإشعارات!", {
            body: "ستتلقى الآن التحديثات المهمة.",
            tag: "welcome",
            duration: 5000
        });
    } else if (permission === "denied") {
        alert("تم حظر الإشعارات. فعّلها من إعدادات المتصفح.");
    }
});

// إرسال أنواع مختلفة من الإشعارات
function notifyNewMessage(sender, message, chatUrl) {
    notifier.show("رسالة جديدة من " + sender, {
        body: message,
        tag: "chat-" + sender.toLowerCase().replace(/\s/g, "-"),
        renotify: true,
        data: { url: chatUrl, type: "message" },
        onClick: (event, data) => {
            window.focus();
            window.location.href = data.url;
        }
    });
}

function notifyTaskDeadline(taskName, timeLeft) {
    notifier.show("اقتراب الموعد النهائي", {
        body: taskName + " مستحقة خلال " + timeLeft + ".",
        tag: "deadline-" + taskName,
        requireInteraction: true,
        data: { url: "/tasks", type: "deadline" }
    });
}

function notifyUploadComplete(fileName) {
    notifier.show("اكتمل الرفع", {
        body: "تم رفع " + fileName + " بنجاح.",
        tag: "upload-complete",
        silent: true,
        duration: 6000
    });
}

// محاكاة أحداث فورية
notifyNewMessage("سارة", "هل يمكنك مراجعة طلب السحب؟", "/chat/sara");
notifyTaskDeadline("تقرير المشروع", "ساعتين");
notifyUploadComplete("presentation.pdf");

مقدمة في إشعارات عامل الخدمة

الإشعارات التي أنشأناها حتى الآن تستخدم واجهة الإشعارات الأساسية التي تتطلب أن تكون الصفحة مفتوحة. لإشعارات الدفع الحقيقية التي تعمل حتى عند إغلاق موقعك، تحتاج إلى عمال الخدمة (Service Workers). تستخدم إشعارات عامل الخدمة self.registration.showNotification() وتدعم ميزات إضافية مثل أزرار الإجراءات.

مثال: أساسيات إشعارات عامل الخدمة

// في ملف JavaScript الرئيسي: تسجيل عامل الخدمة
async function registerServiceWorker() {
    if (!("serviceWorker" in navigator)) {
        console.log("عمال الخدمة غير مدعومين.");
        return null;
    }

    try {
        const registration = await navigator.serviceWorker.register("/sw.js");
        console.log("تم تسجيل عامل الخدمة:", registration.scope);
        return registration;
    } catch (error) {
        console.error("فشل تسجيل عامل الخدمة:", error);
        return null;
    }
}

// عرض إشعار من خلال عامل الخدمة
async function showServiceWorkerNotification(title, options) {
    const registration = await navigator.serviceWorker.ready;

    await registration.showNotification(title, {
        body: options.body || "",
        icon: options.icon || "/images/icon-192.png",
        badge: options.badge || "/images/badge-72.png",
        tag: options.tag || "sw-notification",
        data: options.data || {},
        actions: options.actions || [],
        vibrate: options.vibrate || [200, 100, 200]
    });
}

// الاستخدام
registerServiceWorker().then(() => {
    showServiceWorkerNotification("تحديث في الخلفية", {
        body: "تمت مزامنة بياناتك.",
        tag: "sync-complete",
        actions: [
            { action: "view", title: "عرض التغييرات" },
            { action: "dismiss", title: "تجاهل" }
        ],
        data: { url: "/dashboard" }
    });
});

مثال: التعامل مع أحداث الإشعارات في عامل الخدمة (sw.js)

// sw.js -- ملف عامل الخدمة

// التعامل مع نقر الإشعار
self.addEventListener("notificationclick", function(event) {
    const notification = event.notification;
    const action = event.action;
    const data = notification.data;

    notification.close();

    if (action === "view") {
        event.waitUntil(
            clients.openWindow(data.url || "/")
        );
    } else if (action === "dismiss") {
        console.log("تم تجاهل الإشعار من قبل المستخدم.");
    } else {
        event.waitUntil(
            clients.matchAll({ type: "window" }).then(function(clientList) {
                for (const client of clientList) {
                    if (client.url.includes(data.url) && "focus" in client) {
                        return client.focus();
                    }
                }
                return clients.openWindow(data.url || "/");
            })
        );
    }
});

// التعامل مع إغلاق الإشعار (تم التجاهل بدون نقر)
self.addEventListener("notificationclose", function(event) {
    const data = event.notification.data;
    console.log("تم إغلاق الإشعار بدون تفاعل:", data);
});
نصيحة احترافية: إشعارات عامل الخدمة هي أساس Web Push. ترسل واجهة Push رسائل من خادمك إلى عامل الخدمة، ويستخدم عامل الخدمة showNotification() لعرضها. هذه البنية تمكّن الإشعارات الفورية حتى عندما يكون المستخدم قد أغلق موقعك. مزيج واجهة Push وإشعارات عامل الخدمة هو ما يشغّل أنظمة الإشعارات لتطبيقات الويب التقدمية مثل Twitter وSlack وغيرها من تطبيقات الويب الحديثة.

مثال واقعي: إشعارات تطبيق الدردشة

إليك تنفيذ عملي للإشعارات في تطبيق دردشة. يتعامل مع أنواع مختلفة من الرسائل ويجمّع المحادثات ويدير تكديس الإشعارات بذكاء.

مثال: نظام إشعارات تطبيق الدردشة

class ChatNotificationSystem {
    constructor() {
        this.notifier = new NotificationManager({
            icon: "/images/chat-icon.png"
        });
        this.unreadCounts = new Map();
        this.isWindowFocused = true;

        // تتبع حالة تركيز النافذة
        window.addEventListener("focus", () => {
            this.isWindowFocused = true;
            this.clearAllNotifications();
        });

        window.addEventListener("blur", () => {
            this.isWindowFocused = false;
        });
    }

    // التعامل مع الرسالة الواردة
    onMessageReceived(message) {
        if (this.isWindowFocused && this.isUserOnChatPage(message.senderId)) {
            return;
        }

        const currentCount = this.unreadCounts.get(message.senderId) || 0;
        this.unreadCounts.set(message.senderId, currentCount + 1);

        const count = currentCount + 1;
        const senderName = message.senderName;

        let body;
        if (count === 1) {
            body = message.text;
        } else {
            body = count + " رسائل جديدة";
        }

        this.notifier.show(senderName, {
            body: body,
            tag: "chat-" + message.senderId,
            renotify: true,
            icon: message.senderAvatar || "/images/default-avatar.png",
            data: {
                url: "/chat/" + message.senderId,
                senderId: message.senderId,
                type: "chat-message"
            },
            onClick: (event, data) => {
                window.focus();
                this.navigateToChat(data.senderId);
                this.unreadCounts.delete(data.senderId);
            }
        });
    }

    isUserOnChatPage(senderId) {
        return window.location.pathname === "/chat/" + senderId;
    }

    navigateToChat(senderId) {
        window.location.href = "/chat/" + senderId;
    }

    clearAllNotifications() {
        this.notifier.closeAll();
    }
}

// التهيئة
const chatNotifications = new ChatNotificationSystem();

// محاكاة الرسائل الواردة
chatNotifications.onMessageReceived({
    senderId: "user-101",
    senderName: "سارة",
    senderAvatar: "/images/avatars/sarah.jpg",
    text: "هل يمكنك التحقق من سجلات النشر؟"
});

مثال واقعي: نظام تذكير المهام

يوضح هذا المثال نظام تذكير مهام يجدول الإشعارات على فترات محددة قبل الموعد النهائي ويتعامل مع وظيفة التأجيل.

مثال: نظام إشعارات تذكير المهام

class TaskReminderSystem {
    constructor() {
        this.notifier = new NotificationManager({
            icon: "/images/task-icon.png"
        });
        this.scheduledReminders = new Map();
    }

    // جدولة تذكير لمهمة
    scheduleReminder(task) {
        const now = Date.now();
        const deadline = new Date(task.deadline).getTime();
        const timeUntilDeadline = deadline - now;

        if (timeUntilDeadline <= 0) {
            this.showOverdueNotification(task);
            return;
        }

        const reminders = [];

        // قبل ساعة واحدة
        if (timeUntilDeadline > 3600000) {
            reminders.push({
                delay: timeUntilDeadline - 3600000,
                label: "ساعة واحدة"
            });
        }

        // قبل 15 دقيقة
        if (timeUntilDeadline > 900000) {
            reminders.push({
                delay: timeUntilDeadline - 900000,
                label: "15 دقيقة"
            });
        }

        // عند الموعد النهائي
        reminders.push({
            delay: timeUntilDeadline,
            label: "الآن"
        });

        const timers = reminders.map((reminder) => {
            return setTimeout(() => {
                this.showReminderNotification(task, reminder.label);
            }, reminder.delay);
        });

        this.scheduledReminders.set(task.id, timers);
        console.log("تمت جدولة", timers.length, "تذكيرات لـ:", task.name);
    }

    // عرض إشعار التذكير
    showReminderNotification(task, timeLabel) {
        const isOverdue = timeLabel === "الآن";
        const title = isOverdue ? "حان الموعد النهائي!" : "الموعد النهائي خلال " + timeLabel;

        this.notifier.show(title, {
            body: task.name + (isOverdue
                ? " -- هذه المهمة مستحقة الآن!"
                : " -- مستحقة خلال " + timeLabel + "."),
            tag: "task-" + task.id,
            renotify: true,
            requireInteraction: isOverdue,
            data: {
                taskId: task.id,
                url: "/tasks/" + task.id,
                type: "task-reminder"
            },
            onClick: (event, data) => {
                window.focus();
                window.location.href = data.url;
            }
        });
    }

    // عرض إشعار المهمة المتأخرة
    showOverdueNotification(task) {
        this.notifier.show("مهمة متأخرة!", {
            body: task.name + " كانت مستحقة " + this.getTimeAgo(task.deadline) + ".",
            tag: "overdue-" + task.id,
            requireInteraction: true,
            data: { taskId: task.id, url: "/tasks/" + task.id }
        });
    }

    // إلغاء تذكيرات مهمة
    cancelReminder(taskId) {
        const timers = this.scheduledReminders.get(taskId);
        if (timers) {
            timers.forEach((timer) => clearTimeout(timer));
            this.scheduledReminders.delete(taskId);
            console.log("تم إلغاء تذكيرات المهمة:", taskId);
        }
    }

    // مساعد: الحصول على وقت مقروء منذ
    getTimeAgo(dateString) {
        const diff = Date.now() - new Date(dateString).getTime();
        const minutes = Math.floor(diff / 60000);
        if (minutes < 60) return "منذ " + minutes + " دقيقة";
        const hours = Math.floor(minutes / 60);
        if (hours < 24) return "منذ " + hours + " ساعة";
        return "منذ " + Math.floor(hours / 24) + " يوم";
    }
}

// الاستخدام
const reminders = new TaskReminderSystem();

reminders.scheduleReminder({
    id: "task-001",
    name: "تقديم التقرير الربع سنوي",
    deadline: new Date(Date.now() + 3700000).toISOString()
});

أفضل ممارسات الإشعارات

بناء تجربة إشعارات جيدة يتطلب اهتمامًا دقيقًا بتجربة المستخدم والتوقيت والمحتوى. اتبع هذه الممارسات لضمان أن تكون إشعاراتك مفيدة وليست مزعجة.

  • اطلب في الوقت المناسب -- اطلب الإذن فقط بعد إجراء ذي معنى من المستخدم (النقر على "تفعيل الإشعارات")، وليس أبدًا عند تحميل الصفحة. اشرح لماذا ستكون الإشعارات قيّمة قبل السؤال.
  • اجعل المحتوى موجزًا -- يجب أن تكون عناوين الإشعارات أقل من 50 حرفًا. ويجب أن يكون نص الجسم أقل من 120 حرفًا. المستخدمون يمسحون الإشعارات بسرعة لذا كل كلمة مهمة.
  • استخدم الوسوم للتجميع -- عيّن قيم tag ذات معنى لمنع طوفان الإشعارات. جمّع الإشعارات ذات الصلة (مثل كل الرسائل من نفس الدردشة) تحت نفس الوسم.
  • وفر بديلاً -- نفّذ دائمًا إشعارًا بديلاً داخل التطبيق عندما لا يدعم المتصفح الإشعارات أو رفض المستخدم الإذن.
  • احترم سياق المستخدم -- لا ترسل إشعارات عندما يستخدم المستخدم تطبيقك بنشاط. تحقق من حالة تركيز النافذة قبل تفعيل الإشعارات.
  • اسمح بالتحكم الدقيق -- وفر إعدادات تتيح للمستخدمين اختيار أنواع الإشعارات التي يريدونها (رسائل، تذكيرات، تحديثات) بدلاً من فرض الكل أو لا شيء.
  • حدّد التردد -- نفّذ تحديد المعدل لمنع إغراق المستخدمين بالكثير من الإشعارات في فترة قصيرة. ضع الإشعارات في قائمة انتظار وجمّعها إذا لزم الأمر.
  • ضمّن بيانات قابلة للتنفيذ -- ضمّن دائمًا حمولة data مع رابط أو معرف حتى يأخذ النقر على الإشعار المستخدم إلى المحتوى ذي الصلة.
  • تعامل مع جميع الأحداث -- أرفق دائمًا معالجات onclick وonclose وonerror. لا تترك تفاعلات الإشعارات بدون معالجة.
  • اختبر عبر المنصات -- يختلف مظهر الإشعارات بشكل كبير عبر أنظمة التشغيل (Windows وmacOS وLinux وAndroid). اختبر على جميع المنصات المستهدفة لضمان تجربة جيدة.
مهم: تتطلب العديد من المتصفحات الآن سياقًا آمنًا (HTTPS) لعمل واجهة الإشعارات. لن تعمل الإشعارات على صفحات HTTP العادية في الإنتاج. أثناء التطوير المحلي، يُعامل localhost كسياق آمن، لكن بمجرد النشر يجب أن يستخدم موقعك HTTPS. بالإضافة إلى ذلك، تخنق بعض المتصفحات الإشعارات من التبويبات في الخلفية وقد تسقطها بصمت إذا أُرسل الكثير منها في فترة قصيرة.
ملاحظة: واجهة الإشعارات مختلفة عن واجهة Push، رغم أنهما يُستخدمان معًا غالبًا. واجهة الإشعارات تتعامل مع عرض الإشعارات للمستخدم. واجهة Push تتعامل مع استقبال الرسائل من الخادم عبر عامل الخدمة. عمليًا، تستخدم واجهة Push لاستقبال البيانات وواجهة الإشعارات (عبر عامل الخدمة) لعرضها. هذا الفصل بين المسؤوليات يسمح باستخدام كل واجهة بشكل مستقل أو مجتمعة.

تمرين عملي

ابنِ نظام إشعارات متكاملاً لتطبيق إدارة مشاريع مع المتطلبات التالية: (1) أنشئ فئة NotificationManager تتحقق من الدعم وتطلب الإذن بالتوقيت المناسب (فقط بعد نقر المستخدم على زر "تفعيل") وتخزن حالة الإذن. (2) نفّذ ثلاثة أنواع إشعارات: "مهمة جديدة معيّنة" (مع اسم المهمة والمعيّن ورابط إلى المهمة)، و"تم إضافة تعليق" (مجمّعة حسب المهمة باستخدام الوسوم بحيث تستبدل التعليقات المتعددة على نفس المهمة بعضها البعض)، و"تذكير بالموعد النهائي" (مجدولة للإطلاق قبل 30 دقيقة و5 دقائق من موعد المهمة مع requireInteraction: true). (3) أضف نظام إشعارات toast بديلاً داخل التطبيق يُعرض عند حظر إشعارات المتصفح أو عدم دعمها. (4) استخدم واجهة الأذونات لمراقبة تغييرات الأذونات في الوقت الفعلي وتحديث واجهة المستخدم وفقًا لذلك (أظهر زر "تفعيل" عندما يكون الإذن "prompt"، وأظهر رسالة إعدادات عند "denied"، وأظهر تفضيلات الإشعارات عند "granted"). (5) نفّذ تحديد المعدل بحيث لا يُعرض أكثر من 5 إشعارات خلال أي نافذة زمنية مدتها 30 ثانية. (6) أضف تتبع تركيز النافذة بحيث تُكبت الإشعارات عندما يشاهد المستخدم بنشاط الصفحة ذات الصلة. اختبر نظامك بمحاكاة سلسلة من الأحداث السريعة وتحقق من أن التجميع وتحديد المعدل وتتبع التركيز تعمل جميعها بشكل صحيح.