-
Notifications
You must be signed in to change notification settings - Fork 32
[MOB 5730] Add callbacks to reading/removing in-app messages #557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MOB 5730] Add callbacks to reading/removing in-app messages #557
Conversation
@devcsomnicg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Over all, its looking great.. Added few comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iterable-android-sdk/iterableapi/src/main/java/com/iterable/iterableapi/util/Future.java
Lines 92 to 98 in 520f14c
public interface SuccessCallback<T> { | |
void onSuccess(T result); | |
} | |
public interface FailureCallback { | |
void onFailure(Throwable throwable); | |
} |
Wondering if these ^ existing callbacks can be reused for callbacks instead of creating new Interface ? or will it be good to have them for sanity/separation...
} | ||
|
||
@Test | ||
public void testRemoveMessage() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙌
private IterableNotificationData _notificationData; | ||
private String _deviceId; | ||
private boolean _firstForegroundHandled; | ||
private ResultCallbackHandler callbackHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to see if callback handler should be stored globally in this file. When is the value assigned to this variable? Also because many requests can trigger at same time, this might lead to some race conditions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some more comments
* @param callbackHandler The callback which returns `success` or `failure`. | ||
*/ | ||
public void inAppConsume(@NonNull IterableInAppMessage message, @Nullable IterableInAppDeleteActionType source, @Nullable IterableInAppLocation clickLocation) { | ||
public void inAppConsume(@NonNull IterableInAppMessage message, @Nullable IterableInAppDeleteActionType source, @Nullable IterableInAppLocation clickLocation, @Nullable ResultCallbackHandler callbackHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will need a additional method with new parameter instead of changing this one, as this is a public method which could be used currently - to avoid breaking changes
} | ||
|
||
public void inAppConsume(@NonNull IterableInAppMessage message, @Nullable IterableInAppDeleteActionType source, @Nullable IterableInAppLocation clickLocation, @Nullable String inboxSessionId) { | ||
public void inAppConsume(@NonNull IterableInAppMessage message, @Nullable IterableInAppDeleteActionType source, @Nullable IterableInAppLocation clickLocation, @Nullable String inboxSessionId, @Nullable final ResultCallbackHandler callbackHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use two separate callbacks.
One for success and failure.
And reuse - IterableHelper.success and failure callbacks in the method signature.
} | ||
|
||
apiClient.inAppConsume(message, source, clickLocation, inboxSessionId); | ||
apiClient.inAppConsume(message, source, clickLocation, inboxSessionId, callbackHandler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two callbackHandlers (success and failure) instead of one for consistency.
|
||
if (message.isMarkedForDeletion() && !message.isConsumed()) { | ||
IterableApi.sharedInstance.getInAppManager().removeMessage(message); | ||
IterableApi.sharedInstance.getInAppManager().removeMessage(message, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can stay like before. The new method with new parameters will be for explicit use by developers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments
* @param callbackHandler The callback which returns `success` or `failure`. | ||
*/ | ||
public synchronized void setRead(@NonNull IterableInAppMessage message, boolean read) { | ||
public synchronized void setRead(@NonNull IterableInAppMessage message, boolean read, @Nullable ResultCallbackHandler callbackHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking as read / setRead can be a separate PR of its own. This PR can confine to removeMessage
if(callbackHandler != null){ | ||
callbackHandler.sendResult(true); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be part of next PR for setRead
* @param callbackHandler The callback which returns `success` or `failure`. | ||
*/ | ||
public synchronized void removeMessage(@NonNull IterableInAppMessage message) { | ||
public synchronized void removeMessage(@NonNull IterableInAppMessage message, @Nullable ResultCallbackHandler callbackHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ResultCallbackHandler -> IterableCallbackHandler for success and failure -> in a new method
} | ||
|
||
public synchronized void removeMessage(@NonNull IterableInAppMessage message, @NonNull IterableInAppDeleteActionType source, @NonNull IterableInAppLocation clickLocation) { | ||
public synchronized void removeMessage(@NonNull IterableInAppMessage message, @NonNull IterableInAppDeleteActionType source, @NonNull IterableInAppLocation clickLocation, @Nullable ResultCallbackHandler callbackHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ResultCallbackHandler > Iterable success and failure callback handler.
A new method needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To use Iterable pre existing callbacks
assertEquals(2, inboxMessages.size()); | ||
assertEquals(1, inAppManager.getUnreadInboxMessagesCount()); | ||
inAppManager.removeMessage(inboxMessages.get(0), IterableInAppLocation.inbox, IterableInAppDeleteSource.deleteButton, new IterableHelper.SuccessHandler() { | ||
@Override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverify this block to see if it works as expected as the callback has different handler in the definition above.
|
||
@Test | ||
public void testSetRead() throws Exception { | ||
// Set up mock response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test set read can be part of next PR
@Ayyanchira Changes done as per suggestions. Sorry but setRead changes are in the same PR as it was part of same ticket. Please review and let me know if anything still needs improvement, thanks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some comments
* @param successHandler The callback which returns `success`. | ||
*/ | ||
public synchronized void setRead(@NonNull IterableInAppMessage message, boolean read) { | ||
public synchronized void setRead(@NonNull IterableInAppMessage message, boolean read, @Nullable IterableHelper.SuccessHandler successHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why no failure callback for setRead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Ayyanchira I checked the code but there is no scenario of failure here, that's why I removed an unused param for failure. Please let me know if I am wrong.
@Override | ||
public void onListItemTapped(@NonNull IterableInAppMessage message) { | ||
IterableApi.getInstance().getInAppManager().setRead(message, true); | ||
IterableApi.getInstance().getInAppManager().setRead(message, true, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this PR but inbox fragment can also have placeholders for success and failure callback make use of them..
public void inAppConsume(@NonNull String messageId, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) { | ||
IterableInAppMessage message = getInAppManager().getMessageById(messageId); | ||
if (message == null) { | ||
IterableLogger.e(TAG, "inAppConsume: message is null"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
call failure callback here when we see message is not found.
8c70d0a
into
Iterable:omni-cg-master-pr/mob-5730-callbacks-to-inapp
* add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]>
…583) * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) * add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> * Fixing and adding test method --------- Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”>
commit b374b12 Author: Akshay Ayyanchira <[email protected]> Date: Wed Jun 14 13:24:37 2023 -0700 [MOB 5730] Add callbacks to reading/removing in-app messages (#557) (#583) * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) * add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> * Fixing and adding test method --------- Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”>
* [MOB 5730] Add callbacks to reading/removing in-app messages (#557) (#583) * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) * add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> * Fixing and adding test method --------- Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”> * [MOB - 6493] - Message read and remove bug fix (#592) Co-authored-by: “Akshay <“[email protected]”> * MOB-5132: Fix deep link issue after app is opened from notification (#546) (#593) Co-authored-by: devcsomnicg <[email protected]> * [MOB-6309] prepares EUDC updates for release (#572) * stashed changes * adds data center to config and associated unit tests * adds excluding kotlin files to javadoc check * moves IterableDataRegion to IterableConstants.java * gets rid of extra lines * removes jacoco.exe * adds endpoint override to IterableApi * removes logging statement * sets up base url at IterableRequestTask * minor edits * refactors to pull endpoint directly from config value * removes unfinished unit test * removes jacoco.exec * removes white space --------- Co-authored-by: [email protected] <[email protected]> --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”> Co-authored-by: [email protected] <[email protected]>
commit e8b6532 Author: Evan Takeo Kanaiaupuni Greer <[email protected]> Date: Fri Jul 28 10:18:02 2023 -0600 removes extra dependency (#609) Co-authored-by: [email protected] <[email protected]> commit 6f5a805 Author: Evan Takeo Kanaiaupuni Greer <[email protected]> Date: Mon Jul 17 11:20:49 2023 -0600 [MOB-6309] prepares EUDC updates for release (#572) * stashed changes * adds data center to config and associated unit tests * adds excluding kotlin files to javadoc check * moves IterableDataRegion to IterableConstants.java * gets rid of extra lines * removes jacoco.exe * adds endpoint override to IterableApi * removes logging statement * sets up base url at IterableRequestTask * minor edits * refactors to pull endpoint directly from config value * removes unfinished unit test * removes jacoco.exec * removes white space --------- Co-authored-by: [email protected] <[email protected]> commit b780ec4 Author: Akshay Ayyanchira <[email protected]> Date: Wed Jul 12 06:52:12 2023 -0700 MOB-5132: Fix deep link issue after app is opened from notification (#546) (#593) Co-authored-by: devcsomnicg <[email protected]> commit efa8d3a Author: Akshay Ayyanchira <[email protected]> Date: Wed Jul 12 06:35:59 2023 -0700 [MOB - 6493] - Message read and remove bug fix (#592) Co-authored-by: “Akshay <“[email protected]”> commit b374b12 Author: Akshay Ayyanchira <[email protected]> Date: Wed Jun 14 13:24:37 2023 -0700 [MOB 5730] Add callbacks to reading/removing in-app messages (#557) (#583) * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) * add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> * Fixing and adding test method --------- Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”>
commit 1ed0c34 Author: Akshay Ayyanchira <[email protected]> Date: Thu Aug 31 13:58:56 2023 -0700 Ensure IterableTrampolineActivity always launched (#553) (#618) - Ensure IterableTrampolineActivity always launched to handle all notifications - Reverts back to launchMode="singleInstance" to ensure that the activity is always launched - Use excludeFromRecents="true" to ensure that the activity is not shown in the recent apps list Co-authored-by: Tim Nortman <[email protected]> commit f479175 Author: devcsomnicg <[email protected]> Date: Wed Aug 30 01:40:55 2023 +0530 MOB-5132 (#613) * MOB-5132: Fix deep link issue after app is opened from notification * Fix deep link for buttons It fixes the issue of deep link on buttons --------- Co-authored-by: Akshay Ayyanchira <[email protected]> commit e8b6532 Author: Evan Takeo Kanaiaupuni Greer <[email protected]> Date: Fri Jul 28 10:18:02 2023 -0600 removes extra dependency (#609) Co-authored-by: [email protected] <[email protected]> commit 6f5a805 Author: Evan Takeo Kanaiaupuni Greer <[email protected]> Date: Mon Jul 17 11:20:49 2023 -0600 [MOB-6309] prepares EUDC updates for release (#572) * stashed changes * adds data center to config and associated unit tests * adds excluding kotlin files to javadoc check * moves IterableDataRegion to IterableConstants.java * gets rid of extra lines * removes jacoco.exe * adds endpoint override to IterableApi * removes logging statement * sets up base url at IterableRequestTask * minor edits * refactors to pull endpoint directly from config value * removes unfinished unit test * removes jacoco.exec * removes white space --------- Co-authored-by: [email protected] <[email protected]> commit b780ec4 Author: Akshay Ayyanchira <[email protected]> Date: Wed Jul 12 06:52:12 2023 -0700 MOB-5132: Fix deep link issue after app is opened from notification (#546) (#593) Co-authored-by: devcsomnicg <[email protected]> commit efa8d3a Author: Akshay Ayyanchira <[email protected]> Date: Wed Jul 12 06:35:59 2023 -0700 [MOB - 6493] - Message read and remove bug fix (#592) Co-authored-by: “Akshay <“[email protected]”> commit b374b12 Author: Akshay Ayyanchira <[email protected]> Date: Wed Jun 14 13:24:37 2023 -0700 [MOB 5730] Add callbacks to reading/removing in-app messages (#557) (#583) * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) * add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> * Fixing and adding test method --------- Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”>
* sets custom sound at notification channel * updates notification channel builder to set sound * minor edits * sets sound at notification channel * minor edits * sets up channel id and channel name for each unique sound * adds conditionals for default channel creation * investigating edge cases * debugging * adds default sounds for edge cases * adds default channel id and channel name for edge cases * minor edits * minor edits * cleans up conditionals * adds import for IterableNotificationTest * reorganizes and checks resources folder for matching sound file * minor edits * minor edits * minor edits * updates channel names to sound names * resolves null pointer exception for soundName * removes unneccessary package from build.gradle * README updates * [MOB-5532] - Adding remote notification sound Also removing some checks which seem removable. Still WIP * ChannelId was getting set to null when no sound * Remove all other channels which are not active Remove all the other channels which are not actively showing notification on the notification tray. Warning: Have to check if it only removes the notification created by iterable package? or the entire app's other channels as well. We might have to add iterable_ or some kind of identifier to delete only Iterable ones. * Checkstyle fix * adds resource check for existing local sound and updates soundName checks * adds soundId and soundUrl checks for channel id and channel name * passes soundId down to getSoundUri * Revert "passes soundId down to getSoundUri" This reverts commit 0d2e825. * adds null check to getSoundUri * checkstyle fix * puts sound uri check in createNotificationChannel * reorganizes to do the Uri logic in getSoundUri * checkstyle fixes * addresses comments * reverts back to not deleting inactive channels * Update iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationHelper.java Co-authored-by: Akshay Ayyanchira <[email protected]> * addresses code climate fixes * [MOB-5664] - Revert channel name implementation Revert the code where the channel name was derived by reading value from Android Manifest. If not value is found, a default name "iterable channel" is provided. Now, with custom sound, if a soundURI exists from a provided custom sound, only then soundName is treated as a channel name. Otherwise, old way of handling the name still persists. * [MOB - 5694] - Add try catch around animation Adding try catch around show and hide webview animation * Log warn instead of error checkSDKInitialization logs error message. Log warning message instead * Add firetv/ott support to getmessages call * For firetv call update user with device details upon initialization * Set DEVICE_NOTIFICATIONS_ENABLED key only when registering for push * Fix firetv object structure * Review fixes * [MOB - 5873] - Public getters for Email UserID and Auth _email, _userID and _authToken to accessible by developers. :) * [MOB - 5874] - Schedule auth refresh when auth null Schedule a 10 second auto refresh for auth handler if auth token is found null when initializing. Also, when authHandler receives a authToken from handler in its handler, a similar null check will schedule 10 second refresh throwing a onTokenRegistrationFailure. This callback should allow them to refresh authToken. * create issue in jira when github issue opened * update secret names * add private constructor to prevent instance creation * update unit test * Added import for DeviceInfoUtils * Merge pull request #474 from Iterable/jay/MOB-4666-in-apps-config-memory [MOB-4666] in apps config memory * CI fix Attempt to fix this error * What went wrong: Execution failed for task ':app:processDebugAndroidTestManifest'. > Could not resolve all files for configuration ':app:debugAndroidTestRuntimeClasspath'. > Could not download kappuccino-1.2.1.aar (br.com.concretesolutions:kappuccino:1.2.1) > Could not get resource 'https://jcenter.bintray.com/br/com/concretesolutions/kappuccino/1.2.1/kappuccino-1.2.1.aar'. > Could not GET 'https://jcenter.bintray.com/br/com/concretesolutions/kappuccino/1.2.1/kappuccino-1.2.1.aar'. > Connection reset * [MOB-6055] - [OMNI CG] - Add callbacks to setting email/user id (#570) * Change for supporting callback feature on setEmail/setUserId * fixed failuire callback * add tests for setEmail and setUserId callbacks * changed to iterablehelper callbacks * update test with iterablehelper callbacks * Update IterableApi.java * Retaining the previous method signatures --------- Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”> * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) (#583) * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) * add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> * Fixing and adding test method --------- Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”> * [MOB - 6493] - Message read and remove bug fix (#592) Co-authored-by: “Akshay <“[email protected]”> * MOB-5132: Fix deep link issue after app is opened from notification (#546) (#593) Co-authored-by: devcsomnicg <[email protected]> * [MOB-6309] prepares EUDC updates for release (#572) * stashed changes * adds data center to config and associated unit tests * adds excluding kotlin files to javadoc check * moves IterableDataRegion to IterableConstants.java * gets rid of extra lines * removes jacoco.exe * adds endpoint override to IterableApi * removes logging statement * sets up base url at IterableRequestTask * minor edits * refactors to pull endpoint directly from config value * removes unfinished unit test * removes jacoco.exec * removes white space --------- Co-authored-by: [email protected] <[email protected]> * removes extra dependency (#609) Co-authored-by: [email protected] <[email protected]> * MOB-5132 (#613) * MOB-5132: Fix deep link issue after app is opened from notification * Fix deep link for buttons It fixes the issue of deep link on buttons --------- Co-authored-by: Akshay Ayyanchira <[email protected]> * Ensure IterableTrampolineActivity always launched (#553) (#618) - Ensure IterableTrampolineActivity always launched to handle all notifications - Reverts back to launchMode="singleInstance" to ensure that the activity is always launched - Use excludeFromRecents="true" to ensure that the activity is not shown in the recent apps list Co-authored-by: Tim Nortman <[email protected]> --------- Co-authored-by: evangreer <[email protected]> Co-authored-by: Brad Umbaugh <[email protected]> Co-authored-by: “Akshay <“[email protected]”> Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: amanforindia <[email protected]> Co-authored-by: Ilya Brin <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: Ilya Brin <[email protected]> Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: [email protected] <[email protected]> Co-authored-by: Tim Nortman <[email protected]>
…583) * [MOB 5730] Add callbacks to reading/removing in-app messages (#557) * add callback for setRead/removeMessage * modify test for setRead and added test for removeMessage * fixes * Update build.gradle * removed resultcallbackhandler * Update IterableInAppManager.java * fixes --------- Co-authored-by: Akshay Ayyanchira <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> * Fixing and adding test method --------- Co-authored-by: devcsomnicg <[email protected]> Co-authored-by: Hardik Mashru <[email protected]> Co-authored-by: “Akshay <“[email protected]”>
🔹 JIRA Ticket(s) if any
✏️ Description