Esempio n. 1
0
function* fetchUserReviews({
  payload: { errorHandlerId, page, userId },
}: FetchUserReviewsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  try {
    const state = yield select(getState);

    const params: GetReviewsParams = {
      apiState: state.api,
      page,
      user: userId,
    };

    const response: GetReviewsApiResponse = yield call(getReviews, params);

    yield put(
      setUserReviews({
        pageSize: response.page_size,
        reviewCount: response.count,
        reviews: response.results,
        userId,
      }),
    );
  } catch (error) {
    log.warn(`Failed to load reviews for user ID ${userId}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 2
0
export function* fetchSearchResults({ payload }: SearchStartAction): Saga {
  const { errorHandlerId } = payload;
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const { filters } = payload;

    const state = yield select(getState);

    const params: SearchParams = {
      api: state.api,
      auth: true,
      filters,
    };
    const response = yield call(searchApi, params);
    const { count, page_size: pageSize, results } = response;

    yield put(searchLoad({ count, pageSize, results }));
  } catch (error) {
    log.warn(`Search results failed to load: ${error}`);
    yield put(errorHandler.createErrorAction(error));
    yield put(abortSearch());
  }
}
Esempio n. 3
0
export function* reportAddon({
  payload: { addonSlug, errorHandlerId, message },
}: SendAddonAbuseReportAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const params: ReportAddonParams = {
      addonSlug,
      api: state.api,
      message,
    };
    const response = yield call(reportAddonApi, params);

    yield put(
      loadAddonAbuseReport({
        addon: response.addon,
        message: response.message,
        reporter: response.reporter,
      }),
    );
  } catch (error) {
    log.warn(`Reporting add-on for abuse failed: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
function* fetchReviews({
  payload: { errorHandlerId, addonSlug, page },
}: FetchReviewsAction): Generator<any, any, any> {
  const errorHandler = createErrorHandler(errorHandlerId);
  try {
    const state = yield select(getState);

    const params: GetReviewsParams = {
      addon: addonSlug,
      apiState: state.api,
      // Hide star-only ratings (reviews that do not have a body).
      filter: 'without_empty_body',
      page,
    };

    const response: GetReviewsApiResponse = yield call(getReviews, params);

    yield put(
      setAddonReviews({
        addonSlug,
        pageSize: response.page_size,
        reviewCount: response.count,
        reviews: response.results,
      }),
    );
  } catch (error) {
    log.warn(`Failed to load reviews for add-on slug ${addonSlug}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 5
0
export function* fetchDiscoveryAddons({
  payload: { errorHandlerId, taarParams },
}: GetDiscoResultsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  try {
    const state = yield select(getState);

    const { results }: ExternalDiscoResultsType = yield call(
      getDiscoveryAddons,
      {
        api: state.api,
        taarParams,
      },
    );

    const addons = createExternalAddonMap({ results });

    yield put(loadAddonResults({ addons }));
    yield put(loadDiscoResults({ results }));
  } catch (error) {
    log.warn(`Failed to fetch discovery add-ons: ${error}`);

    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 6
0
function* fetchGroupedRatings({
  payload: { errorHandlerId, addonId },
}: FetchGroupedRatingsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);
  try {
    const state = yield select(getState);

    const params: GetReviewsParams = {
      addon: addonId,
      apiState: state.api,
      show_grouped_ratings: true,
    };
    const response: GetReviewsApiResponse = yield call(getReviews, params);

    if (!response.grouped_ratings) {
      // This is unlikely to happen but if it does we should stop the show.
      throw new Error(
        oneLine`The request to getReviews({ show_grouped_ratings: true })
        unexpectedly returned an empty grouped_ratings object`,
      );
    }
    yield put(
      setGroupedRatings({
        addonId,
        grouping: response.grouped_ratings,
      }),
    );
  } catch (error) {
    log.warn(
      `Failed to fetch grouped ratings for add-on ID ${addonId}: ${error}`,
    );
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 7
0
function* fetchReviews({
  payload: { errorHandlerId, addonSlug, page, score },
}: FetchReviewsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  try {
    yield put(errorHandler.createClearingAction());
    const state = yield select(getState);

    const params: GetReviewsParams = {
      addon: addonSlug,
      apiState: state.api,
      page,
      score: score || undefined,
    };

    const response: GetReviewsApiResponse = yield call(getReviews, params);

    yield put(
      setAddonReviews({
        addonSlug,
        page: page || '1',
        pageSize: response.page_size,
        reviewCount: response.count,
        reviews: response.results,
        score,
      }),
    );
  } catch (error) {
    log.warn(`Failed to load reviews for add-on slug ${addonSlug}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 8
0
function* fetchReviewPermissions({
  payload: { errorHandlerId, addonId, userId },
}: FetchReviewPermissionsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  try {
    yield put(errorHandler.createClearingAction());
    const state: AppState = yield select(getState);

    const params: GetReviewsParams = {
      addon: addonId,
      apiState: state.api,
      show_permissions_for: userId,
    };

    const response: GetReviewsApiResponse = yield call(getReviews, params);
    invariant(
      response.can_reply !== undefined,
      'response.can_reply was unexpectedly undefined',
    );

    yield put(
      setReviewPermissions({
        addonId,
        canReplyToReviews: response.can_reply,
        userId,
      }),
    );
  } catch (error) {
    log.warn(
      `Failed to load review permissions for add-on ID ${addonId}, user ID ${userId}: ${error}`,
    );
    yield put(errorHandler.createErrorAction(error));
  }
}
export function* fetchLandingAddons({
  payload: { addonType, category, errorHandlerId },
}) {
  const errorHandler = createErrorHandler(errorHandlerId);
  try {
    const state = yield select(getState);
    const { api } = state;
    const filters = {
      addonType: getAddonTypeFilter(addonType),
      page_size: isTheme(addonType)
        ? LANDING_PAGE_THEME_COUNT
        : LANDING_PAGE_EXTENSION_COUNT,
    };

    if (category) {
      filters.category = category;
    }

    const [featured, highlyRated, trending] = yield all([
      call(searchApi, {
        api,
        filters: {
          ...filters,
          featured: true,
          sort: SEARCH_SORT_RANDOM,
        },
        page: 1,
      }),
      call(searchApi, {
        api,
        filters: { ...filters, sort: SEARCH_SORT_TOP_RATED },
        page: 1,
      }),
      call(searchApi, {
        api,
        filters: { ...filters, sort: SEARCH_SORT_TRENDING },
        page: 1,
      }),
    ]);

    yield put(
      loadLanding({
        addonType,
        featured,
        highlyRated,
        trending,
      }),
    );
  } catch (error) {
    log.warn(oneLine`Failed to fetch landing page add-ons for
      addonType ${addonType}: ${error}`);

    yield put(errorHandler.createErrorAction(error));
  }
}
export function* fetchAddonsByAuthors({ payload }) {
  const {
    addonType,
    authorUsernames,
    errorHandlerId,
    forAddonSlug,
    page,
    pageSize,
    sort,
  } = payload;

  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const response = yield call(searchApi, {
      api: state.api,
      filters: {
        addonType: getAddonTypeFilter(addonType),
        author: authorUsernames.sort().join(','),
        exclude_addons: forAddonSlug,
        page: page || 1,
        page_size: pageSize,
        sort: sort || SEARCH_SORT_TRENDING,
      },
    });

    // TODO: remove the line below and pass `response.addons` directly once
    // https://github.com/mozilla/addons-frontend/issues/2917 is done.
    const addons = Object.values(response.entities.addons || {});
    const { count } = response.result;

    yield put(
      loadAddonsByAuthors({
        addonType,
        addons,
        authorUsernames,
        count,
        forAddonSlug,
        pageSize,
      }),
    );
  } catch (error) {
    log.warn(`Search for addons by authors results failed to load: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 11
0
export function* fetchAddonsByAuthors({
  payload,
}: FetchAddonsByAuthorsAction): Saga {
  const {
    addonType,
    authorIds,
    errorHandlerId,
    forAddonSlug,
    page,
    pageSize,
    sort,
  } = payload;

  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const params: SearchParams = {
      api: state.api,
      filters: {
        addonType: getAddonTypeFilter(addonType),
        author: authorIds.join(','),
        exclude_addons: forAddonSlug,
        page: page || '1',
        page_size: pageSize,
        sort: sort || SEARCH_SORT_TRENDING,
      },
    };
    const response = yield call(searchApi, params);

    const { count, results } = response;

    yield put(
      loadAddonsByAuthors({
        addonType,
        addons: results,
        authorIds,
        count,
        forAddonSlug,
        pageSize,
      }),
    );
  } catch (error) {
    log.warn(`Search for addons by authors results failed to load: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
export function* fetchLanguageTools({ payload: { errorHandlerId } }) {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const response = yield call(languageToolsApi, { api: state.api });

    yield put(loadLanguageTools({ languageTools: response.results }));
  } catch (error) {
    log.warn(`Loading Language tools failed: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 13
0
function* fetchLatestUserReview({
  payload: { addonId, errorHandlerId, userId },
}: FetchLatestUserReviewAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state: AppState = yield select(getState);

    const params: GetLatestUserReviewParams = {
      addon: addonId,
      apiState: state.api,
      user: userId,
    };

    const review: GetLatestUserReviewResponse = yield call(
      getLatestUserReview,
      params,
    );

    const _setLatestReview = (value) => {
      return setLatestReview({
        userId,
        addonId,
        review: value,
      });
    };

    if (review) {
      yield put(setReview(review));
      yield put(_setLatestReview(review));
    } else {
      log.debug(
        `No saved review found for userId ${userId}, addonId ${addonId}`,
      );
      yield put(_setLatestReview(null));
    }
  } catch (error) {
    log.warn(
      `Failed to fetchLatestUserReview for addonId "${addonId}", userId "${userId}": ${error}`,
    );
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 14
0
export function* unsubscribeNotification({
  payload: { errorHandlerId, hash, token, notification },
}: UnsubscribeNotificationAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const params: UnsubscribeNotificationParams = {
      api: state.api,
      hash,
      notification,
      token,
    };

    yield call(api.unsubscribeNotification, params);

    yield put(
      finishUnsubscribeNotification({
        hash,
        notification,
        token,
      }),
    );
  } catch (error) {
    log.warn(
      `Could not unsubscribe from ${notification} notification: ${error}`,
    );
    yield put(errorHandler.createErrorAction(error));
    yield put(
      abortUnsubscribeNotification({
        hash,
        notification,
        token,
      }),
    );
  }
}
Esempio n. 15
0
function* handleFlagReview({
  payload: { errorHandlerId, note, reason, reviewId },
}: FlagReviewAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);
    yield call(flagReview, {
      apiState: state.api,
      note,
      reason,
      reviewId,
    });

    yield put(setReviewWasFlagged({ reason, reviewId }));
  } catch (error) {
    log.warn(`Failed to flag review ID ${reviewId}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 16
0
function* deleteAddonReview({
  payload: { addonId, errorHandlerId, isReplyToReviewId, reviewId },
}: DeleteAddonReviewAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);
    yield call(deleteReview, {
      apiState: state.api,
      reviewId,
    });

    yield put(unloadAddonReviews({ addonId, reviewId }));
    if (isReplyToReviewId) {
      yield put(unloadAddonReviews({ addonId, reviewId: isReplyToReviewId }));
    }
  } catch (error) {
    log.warn(`Failed to delete review ID ${reviewId}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 17
0
export function* fetchUserNotifications({
  payload: { errorHandlerId, userId },
}: FetchUserNotificationsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const params: UserApiParams = {
      api: state.api,
      userId,
    };

    const notifications = yield call(api.userNotifications, params);

    yield put(loadUserNotifications({ notifications, userId }));
  } catch (error) {
    log.warn(`User notifications failed to load: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 18
0
export function* fetchVersions({
  payload: { errorHandlerId, page, slug },
}: FetchVersionsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const params: GetVersionsParams = {
      api: state.api,
      page,
      slug,
    };
    const versions = yield call(getVersions, params);

    yield put(loadVersions({ slug, versions }));
  } catch (error) {
    log.warn(`Failed to fetch versions: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 19
0
function* fetchReview({
  payload: { errorHandlerId, reviewId },
}: FetchReviewAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const params: GetReviewParams = {
      apiState: state.api,
      reviewId,
    };

    const review: ExternalReviewType = yield call(getReview, params);

    yield put(setReview(review));
  } catch (error) {
    log.warn(`Failed to get review ID ${reviewId}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 20
0
export function* deleteUserAccount({
  payload: { errorHandlerId, userId },
}: DeleteUserAccountAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const params: UserApiParams = {
      api: state.api,
      userId,
    };

    yield call(api.deleteUserAccount, params);

    yield put(unloadUserAccount({ userId }));
  } catch (error) {
    log.warn(`Could not delete user account: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 21
0
export function* fetchReviews(
  {
    payload: { errorHandlerId, addonSlug, page },
  }: FetchReviewsAction
): Generator<any, any, any> {
  const errorHandler = createErrorHandler(errorHandlerId);
  try {
    yield put(showLoading());
    const api = yield select(getApi);
    const response = yield call(getReviews, {
      // Hide star-only ratings (reviews that do not have a body).
      api, addon: addonSlug, page, filter: 'without_empty_body',
    });
    yield put(setAddonReviews({
      addonSlug, reviews: response.results, reviewCount: response.count,
    }));
  } catch (error) {
    log.warn(`Failed to load reviews for add-on slug ${addonSlug}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  } finally {
    yield put(hideLoading());
  }
}
Esempio n. 22
0
function* handleReplyToReview({
  payload: { errorHandlerId, originalReviewId, body, title },
}: SendReplyToReviewAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);
    const reviewResponse: ExternalReviewReplyType = yield call(replyToReview, {
      apiState: state.api,
      body,
      originalReviewId,
      title,
    });

    yield put(setReviewReply({ originalReviewId, reply: reviewResponse }));

    yield put(hideReplyToReviewForm({ reviewId: originalReviewId }));
  } catch (error) {
    log.warn(`Failed to send reply to review ID ${originalReviewId}: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  }
}
Esempio n. 23
0
export function* fetchRecommendations({
  payload: { errorHandlerId, guid, recommended },
}: FetchRecommendationsAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);
  yield put(errorHandler.createClearingAction());

  try {
    invariant(typeof recommended === 'boolean', 'recommended is required');
    const state = yield select(getState);

    const params: GetRecommendationsParams = {
      api: state.api,
      guid,
      recommended,
    };
    const recommendations = yield call(api.getRecommendations, params);
    const {
      fallback_reason: fallbackReason,
      outcome,
      results: addons,
    } = recommendations;

    yield put(
      loadRecommendations({
        addons,
        fallbackReason,
        guid,
        outcome,
      }),
    );
  } catch (error) {
    log.warn(`Failed to recommendations: ${error}`);
    yield put(errorHandler.createErrorAction(error));
    yield put(abortFetchRecommendations({ guid }));
  }
}
Esempio n. 24
0
export function* updateUserAccount({
  payload: {
    errorHandlerId,
    notifications,
    picture,
    pictureData,
    userFields,
    userId,
  },
}: UpdateUserAccountAction): Saga {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  try {
    const state = yield select(getState);

    const userAccountParams: UpdateUserAccountParams = {
      api: state.api,
      picture,
      userId,
      ...userFields,
    };

    const user = yield call(api.updateUserAccount, userAccountParams);

    if (picture) {
      // The post-upload task (resize, etc.) is asynchronous so we set the
      // uploaded file before loading the user account in order to display the
      // latest picture.
      // See: https://github.com/mozilla/addons-frontend/issues/5252
      user.picture_url = pictureData;
    }

    yield put(loadUserAccount({ user }));

    if (Object.keys(notifications).length) {
      const params: UpdateUserNotificationsParams = {
        api: state.api,
        notifications,
        userId,
      };
      const allNotifications = yield call(api.updateUserNotifications, params);

      if (typeof notifications.announcements !== 'undefined') {
        // The Salesforce integration is asynchronous and takes a lot of time
        // so we set the notification to whatever the user has chosen,
        // otherwise we would display the wrong notification value.
        // See: https://github.com/mozilla/addons-frontend/issues/5219
        const index = allNotifications.findIndex(
          (notification) => notification.name === 'announcements',
        );
        if (index !== -1) {
          allNotifications[index].enabled = notifications.announcements;
          log.debug(
            'Optimistically set user value for "announcements" notification',
          );
        }
      }

      yield put(
        loadUserNotifications({
          notifications: allNotifications,
          userId: user.id,
        }),
      );
    }
  } catch (error) {
    log.warn(`Could not update user account: ${error}`);
    yield put(errorHandler.createErrorAction(error));
  } finally {
    yield put(finishUpdateUserAccount());
  }
}
export function* fetchHomeAddons({
  payload: { collectionsToFetch, errorHandlerId, includeFeaturedThemes },
}: FetchHomeAddonsAction): Generator<any, any, any> {
  const errorHandler = createErrorHandler(errorHandlerId);

  yield put(errorHandler.createClearingAction());

  const state = yield select(getState);

  const collections = [];
  for (const collection of collectionsToFetch) {
    try {
      const params: GetCollectionAddonsParams = {
        api: state.api,
        slug: collection.slug,
        username: collection.username,
      };
      const result = yield call(getCollectionAddons, params);
      collections.push(result);
    } catch (error) {
      log.warn(
        oneLine`Home collection: ${collection.username}/${collection.slug}
          failed to load: ${error}`,
      );
      if (error.response && [401, 404].includes(error.response.status)) {
        // The collection was not found or is marked private.
        collections.push(null);
      } else {
        yield put(errorHandler.createErrorAction(error));
        return;
      }
    }
  }

  const featuredSearchFilters = {
    featured: true,
    page_size: LANDING_PAGE_EXTENSION_COUNT,
    sort: SEARCH_SORT_RANDOM,
  };
  const featuredExtensionsParams: SearchParams = {
    api: state.api,
    filters: {
      addonType: ADDON_TYPE_EXTENSION,
      ...featuredSearchFilters,
    },
  };
  const featuredThemesParams: SearchParams = {
    api: state.api,
    filters: {
      addonType: getAddonTypeFilter(ADDON_TYPE_THEME),
      ...featuredSearchFilters,
      page_size: LANDING_PAGE_THEME_COUNT,
    },
  };
  const popularExtensionsParams: SearchParams = {
    api: state.api,
    filters: {
      addonType: ADDON_TYPE_EXTENSION,
      page_size: LANDING_PAGE_EXTENSION_COUNT,
      sort: SEARCH_SORT_POPULAR,
    },
  };

  let homeAddons = {};
  try {
    homeAddons = yield all({
      featuredExtensions: call(searchApi, featuredExtensionsParams),
      featuredThemes: includeFeaturedThemes
        ? call(searchApi, featuredThemesParams)
        : null,
      popularExtensions: call(searchApi, popularExtensionsParams),
    });
  } catch (error) {
    log.warn(`Home add-ons failed to load: ${error}`);
    yield put(errorHandler.createErrorAction(error));
    return;
  }

  yield put(
    loadHomeAddons({
      collections,
      featuredExtensions: homeAddons.featuredExtensions,
      featuredThemes: homeAddons.featuredThemes,
      popularExtensions: homeAddons.popularExtensions,
    }),
  );
}
Esempio n. 26
0
function* manageAddonReview(
  action: CreateAddonReviewAction | UpdateAddonReviewAction,
  { _delay = delay }: Options = {},
) {
  const { body, errorHandlerId, score } = action.payload;
  const errorHandler = createErrorHandler(errorHandlerId);

  const savingRating = !!score;
  const savingReview = !!body;

  yield put(errorHandler.createClearingAction());
  if (savingRating) {
    yield put(flashReviewMessage(STARTED_SAVE_RATING));
  }
  if (savingReview) {
    yield put(flashReviewMessage(STARTED_SAVE_REVIEW));
  }

  try {
    const state: AppState = yield select(getState);
    const baseParams = {
      apiState: state.api,
      body,
      score,
    };
    let params;

    let oldReview = null;
    if (action.type === CREATE_ADDON_REVIEW) {
      params = {
        ...baseParams,
        addonId: action.payload.addonId,
        versionId: action.payload.versionId,
      };
    } else if (action.type === UPDATE_ADDON_REVIEW) {
      params = {
        ...baseParams,
        reviewId: action.payload.reviewId,
      };
      oldReview = selectReview(state.reviews, action.payload.reviewId);
      invariant(
        oldReview,
        `review with ID=${action.payload.reviewId} does not exist in state`,
      );
    }
    invariant(
      params,
      `params was unexpectedly empty; action.type: ${action.type}`,
    );

    const submitParams: SubmitReviewParams = params;
    const reviewFromResponse: SubmitReviewResponse = yield call(
      submitReview,
      submitParams,
    );

    yield put(setReview(reviewFromResponse));

    if (savingRating) {
      yield put(flashReviewMessage(SAVED_RATING));
    }
    if (savingReview) {
      yield put(flashReviewMessage(SAVED_REVIEW));
      yield put(hideEditReviewForm({ reviewId: reviewFromResponse.id }));
    }

    if (!reviewFromResponse.is_developer_reply) {
      yield put(
        setLatestReview({
          addonId: reviewFromResponse.addon.id,
          review: reviewFromResponse,
          userId: reviewFromResponse.user.id,
        }),
      );

      yield put(
        updateRatingCounts({
          addonId: reviewFromResponse.addon.id,
          oldReview,
          newReview: createInternalReview(reviewFromResponse),
        }),
      );
    }

    // Make the message disappear after some time.
    yield _delay(FLASH_SAVED_MESSAGE_DURATION);
    yield put(hideFlashedReviewMessage());
  } catch (error) {
    log.warn(
      `Failed to create/update review with action ${action.type}: ${error}`,
    );
    yield put(errorHandler.createErrorAction(error));
    yield put(flashReviewMessage(ABORTED));
  }
}