diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java b/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java index 0cadcba646..5fac8da9ae 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java @@ -1,7 +1,9 @@ package fr.free.nrw.commons.achievements; +import android.util.Log; + /** - * represnts Achievements + * represnts Achievements class ans stores all the parameters */ public class Achievements { private int uniqueUsedImages; @@ -10,24 +12,41 @@ public class Achievements { private int imagesEditedBySomeoneElse; private int featuredImages; private int imagesUploaded; + private int revertCount; public Achievements(){ } + /** + * constructor for achievements class to set its data members + * @param uniqueUsedImages + * @param articlesUsingImages + * @param thanksReceived + * @param imagesEditedBySomeoneElse + * @param featuredImages + * @param imagesUploaded + * @param revertCount + */ public Achievements(int uniqueUsedImages, int articlesUsingImages, int thanksReceived, int imagesEditedBySomeoneElse, int featuredImages, - int imagesUploaded) { + int imagesUploaded, + int revertCount) { this.uniqueUsedImages = uniqueUsedImages; this.articlesUsingImages = articlesUsingImages; this.thanksReceived = thanksReceived; this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse; this.featuredImages = featuredImages; this.imagesUploaded = imagesUploaded; + this.revertCount = revertCount; } + + /** + * Builder class for Achievements class + */ public class AchievementsBuilder { private int nestedUniqueUsedImages; private int nestedArticlesUsingImages; @@ -35,6 +54,7 @@ public class AchievementsBuilder { private int nestedImagesEditedBySomeoneElse; private int nestedFeaturedImages; private int nestedImagesUploaded; + private int nestedRevertCount; public AchievementsBuilder setUniqueUsedImages(int uniqueUsedImages) { this.nestedUniqueUsedImages = uniqueUsedImages; @@ -66,62 +86,120 @@ public AchievementsBuilder setImagesUploaded(int imagesUploaded) { return this; } + public AchievementsBuilder setRevertCount( int revertCount){ + this.nestedRevertCount = revertCount; + return this; + } + public Achievements createAchievements(){ return new Achievements(nestedUniqueUsedImages, nestedArticlesUsingImages, nestedThanksReceived, nestedImagesEditedBySomeoneElse, nestedFeaturedImages, - nestedImagesUploaded); + nestedImagesUploaded, + nestedRevertCount); } } + /** + * getter function to get count of images uploaded + * @return + */ public int getImagesUploaded() { return imagesUploaded; } + /** + * getter function to get count of featured images + * @return + */ public int getFeaturedImages() { return featuredImages; } - public int getImagesEditedBySomeoneElse() { - return imagesEditedBySomeoneElse; - } - + /** + * getter function to get count of thanks received + * @return + */ public int getThanksReceived() { return thanksReceived; } - public int getArticlesUsingImages() { - return articlesUsingImages; - } - + /** + * getter function to get count of unique images used by wiki + * @return + */ public int getUniqueUsedImages() { return uniqueUsedImages; } + /** + * setter function to count of images uploaded + * @param imagesUploaded + */ public void setImagesUploaded(int imagesUploaded) { this.imagesUploaded = imagesUploaded; } + /** + * setter function to set count of featured images + * @param featuredImages + */ public void setFeaturedImages(int featuredImages) { this.featuredImages = featuredImages; } + /** + * setter function to set the count of images edited by someone + * @param imagesEditedBySomeoneElse + */ public void setImagesEditedBySomeoneElse(int imagesEditedBySomeoneElse) { this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse; } + /** + * setter function to set count of thanks received + * @param thanksReceived + */ public void setThanksReceived(int thanksReceived) { this.thanksReceived = thanksReceived; } + /** + * setter function to count of articles using images uploaded + * @param articlesUsingImages + */ public void setArticlesUsingImages(int articlesUsingImages) { this.articlesUsingImages = articlesUsingImages; } + /** + * setter function to set count of uniques images used by wiki + * @param uniqueUsedImages + */ public void setUniqueUsedImages(int uniqueUsedImages) { this.uniqueUsedImages = uniqueUsedImages; } + + /** + * to set count of images reverted + * @param revertCount + */ + public void setRevertCount(int revertCount) { + this.revertCount = revertCount; + } + + /** + * used to calculate the percentages of images that haven't been reverted + * @return + */ + public int getNotRevertPercentage(){ + try { + return ((imagesUploaded - revertCount) * 100)/imagesUploaded; + } catch (ArithmeticException divideByZero ){ + return 100; + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java index ec9cbf6ada..905d0dfbd7 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java @@ -1,6 +1,8 @@ package fr.free.nrw.commons.achievements; +import android.accounts.Account; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -38,6 +40,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Optional; import javax.inject.Inject; @@ -49,6 +52,7 @@ import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; @@ -63,8 +67,8 @@ public class AchievementsActivity extends NavigationBaseActivity { private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3; private Boolean isUploadFetched = false; private Boolean isStatisticsFetched = false; + private Boolean isRevertFetched = false; private Achievements achievements = new Achievements(); - private LevelController level; private LevelController.LevelInfo levelInfo; @BindView(R.id.achievement_badge) @@ -79,8 +83,12 @@ public class AchievementsActivity extends NavigationBaseActivity { CircleProgressBar imagesUploadedProgressbar; @BindView(R.id.images_used_by_wiki_progressbar) CircleProgressBar imagesUsedByWikiProgessbar; + @BindView(R.id.image_reverts_progressbar) + CircleProgressBar imageRevertsProgressbar; @BindView(R.id.image_featured) TextView imagesFeatured; + @BindView(R.id.images_revert_limit_text) + TextView imagesRevertLimitText; @BindView(R.id.progressBar) ProgressBar progressBar; @BindView(R.id.layout_image_uploaded) @@ -133,6 +141,7 @@ protected void onCreate(Bundle savedInstanceState) { hideLayouts(); setAchievements(); setUploadCount(); + setRevertCount(); initDrawer(); } @@ -141,14 +150,8 @@ protected void onCreate(Bundle savedInstanceState) { */ @OnClick(R.id.achievement_info) public void showInfoDialog(){ - new AlertDialog.Builder(AchievementsActivity.this) - .setTitle(R.string.Achievements) - .setMessage(R.string.achievements_info_message) - .setCancelable(true) - .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) - .create() - .show(); - + launchAlert(getResources().getString(R.string.Achievements) + ,getResources().getString(R.string.achievements_info_message)); } @Override @@ -198,27 +201,72 @@ void shareScreen(Bitmap bitmap) { * which then calls parseJson when results are fetched */ private void setAchievements() { - compositeDisposable.add(mediaWikiApi - .getAchievements(sessionManager.getCurrentAccount().name) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - jsonObject -> parseJson(jsonObject) - )); + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getAchievements(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + jsonObject -> parseJson(jsonObject), + t -> Timber.e(t, "Fetching achievements statisticss failed") + )); + } + } + + /** + * To call the API to get reverts count in form of JSONObject + * + */ + + private void setRevertCount(){ + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getRevertCount(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + object -> parseJsonRevertCount(object), + t -> Timber.e(t, "Fetching revert count failed") + )); + } + } + + /** + * used to set number of deleted images + * @param object + */ + private void parseJsonRevertCount(JSONObject object){ + try { + achievements.setRevertCount(object.getInt("deletedUploads")); + } catch (JSONException e) { + Timber.d( e, e.getMessage()); + } + isRevertFetched = true; + hideProgressBar(); } /** * used to the count of images uploaded by user */ private void setUploadCount() { - compositeDisposable.add(mediaWikiApi - .getUploadCount(sessionManager.getCurrentAccount().name) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> achievements.setImagesUploaded(uploadCount), - t -> Timber.e(t, "Fetching upload count failed") - )); + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getUploadCount(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + uploadCount -> setAchievementsUploadCount(uploadCount), + t -> Timber.e(t, "Fetching upload count failed") + )); + } + } + + /** + * used to set achievements upload count and call hideProgressbar + * @param uploadCount + */ + private void setAchievementsUploadCount(int uploadCount){ + achievements.setImagesUploaded(uploadCount); isUploadFetched = true; hideProgressBar(); } @@ -227,17 +275,26 @@ private void setUploadCount() { * used to the uploaded images progressbar * @param uploadCount */ - private void setUploadProgress( int uploadCount){ - + private void setUploadProgress(int uploadCount){ imagesUploadedProgressbar.setProgress (100*uploadCount/levelInfo.getMaxUploadCount()); imagesUploadedProgressbar.setProgressTextFormatPattern (uploadCount +"/" + levelInfo.getMaxUploadCount() ); } + /** + * used to set the non revert image percentage + * @param notRevertPercentage + */ + private void setImageRevertPercentage(int notRevertPercentage){ + imageRevertsProgressbar.setProgress(notRevertPercentage); + String revertPercentage = Integer.toString(notRevertPercentage); + imageRevertsProgressbar.setProgressTextFormatPattern(revertPercentage + "%%"); + imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); + } + /** * used to parse the JSONObject containing results - * * @param object */ private void parseJson(JSONObject object) { @@ -262,7 +319,7 @@ private void parseJson(JSONObject object) { * and assign badge and level * @param achievements */ - private void inflateAchievements( Achievements achievements ){ + private void inflateAchievements(Achievements achievements ){ thanksReceived.setText(Integer.toString(achievements.getThanksReceived())); imagesUsedByWikiProgessbar.setProgress (100*achievements.getUniqueUsedImages()/levelInfo.getMaxUniqueImages() ); @@ -281,7 +338,6 @@ private void inflateAchievements( Achievements achievements ){ /** * Creates a way to change current activity to AchievementActivity - * * @param context */ public static void startYourself(Context context) { @@ -295,10 +351,13 @@ public static void startYourself(Context context) { * to hide progressbar */ private void hideProgressBar() { - if (progressBar != null && isUploadFetched && isStatisticsFetched) { - levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(),achievements.getUniqueUsedImages()); + if (progressBar != null && isUploadFetched && isStatisticsFetched && isRevertFetched) { + levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), + achievements.getUniqueUsedImages(), + achievements.getNotRevertPercentage()); inflateAchievements(achievements); setUploadProgress(achievements.getImagesUploaded()); + setImageRevertPercentage(achievements.getNotRevertPercentage()); progressBar.setVisibility(View.GONE); layoutImageReverts.setVisibility(View.VISIBLE); layoutImageUploaded.setVisibility(View.VISIBLE); @@ -348,4 +407,52 @@ public void onClick(DialogInterface dialog, int which) { alertadd.show(); } + @OnClick(R.id.images_upload_info) + public void showUploadInfo(){ + launchAlert(getResources().getString(R.string.images_uploaded) + ,getResources().getString(R.string.images_uploaded_explanation)); + } + + @OnClick(R.id.images_reverted_info) + public void showRevertedInfo(){ + launchAlert(getResources().getString(R.string.image_reverts) + ,getResources().getString(R.string.images_reverted_explanation)); + } + + @OnClick(R.id.images_used_by_wiki_info) + public void showUsedByWikiInfo(){ + launchAlert(getResources().getString(R.string.images_used_by_wiki) + ,getResources().getString(R.string.images_used_explanation)); + } + + /** + * takes title and message as input to display alerts + * @param title + * @param message + */ + private void launchAlert(String title, String message){ + new AlertDialog.Builder(AchievementsActivity.this) + .setTitle(title) + .setMessage(message) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show(); + } + + /** + * check to ensure that user is logged in + * @return + */ + private boolean checkAccount(){ + Account currentAccount = sessionManager.getCurrentAccount(); + if(currentAccount == null) { + Timber.d("Current account is null"); + ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in)); + sessionManager.forceLogin(this); + return false; + } + return true; + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java index 20176759dc..e0f84bbee9 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.achievements; +import android.util.Log; + import fr.free.nrw.commons.R; /** @@ -9,39 +11,49 @@ public class LevelController { public LevelInfo level; public enum LevelInfo{ - LEVEL_15(15,R.style.LevelFive, 80, 160), - LEVEL_14(14,R.style.LevelFour, 75 , 150), - LEVEL_13(13,R.style.LevelThree, 70, 140), - LEVEL_12(12,R.style.LevelTwo,65 , 130), - LEVEL_11(11,R.style.LevelOne, 60, 120), - LEVEL_10(10, R.style.LevelFive, 55, 110), - LEVEL_9(9, R.style.LevelFour, 50, 100), - LEVEL_8(8, R.style.LevelThree, 45, 90), - LEVEL_7(7, R.style.LevelTwo, 40, 80), - LEVEL_6(6,R.style.LevelOne,30,70), - LEVEL_5(5, R.style.LevelFive, 25, 60), - LEVEL_4(4, R.style.LevelFour,20,50), - LEVEL_3(3, R.style.LevelThree, 15,40), - LEVEL_2(2, R.style.LevelTwo, 10, 30), - LEVEL_1(1, R.style.LevelOne, 5, 20 ); + LEVEL_1(1, R.style.LevelOne, 5, 20, 85), + LEVEL_2(2, R.style.LevelTwo, 10, 30, 86), + LEVEL_3(3, R.style.LevelThree, 15,40, 87), + LEVEL_4(4, R.style.LevelFour,20,50, 88), + LEVEL_5(5, R.style.LevelFive, 25, 60, 89), + LEVEL_6(6,R.style.LevelOne,30,70, 90), + LEVEL_7(7, R.style.LevelTwo, 40, 80, 90), + LEVEL_8(8, R.style.LevelThree, 45, 90, 90), + LEVEL_9(9, R.style.LevelFour, 50, 100, 90), + LEVEL_10(10, R.style.LevelFive, 55, 110, 90), + LEVEL_11(11,R.style.LevelOne, 60, 120, 90), + LEVEL_12(12,R.style.LevelTwo,65 , 130, 90), + LEVEL_13(13,R.style.LevelThree, 70, 140, 90), + LEVEL_14(14,R.style.LevelFour, 75 , 150, 90), + LEVEL_15(15,R.style.LevelFive, 80, 160, 90); private int levelNumber; private int levelStyle; private int maxUniqueImages; private int maxUploadCount; + private int minNonRevertPercentage; - LevelInfo(int levelNumber, int levelStyle, int maxUniqueImages, int maxUploadCount) { + LevelInfo(int levelNumber, + int levelStyle, + int maxUniqueImages, + int maxUploadCount, + int minNonRevertPercentage) { this.levelNumber = levelNumber; this.levelStyle = levelStyle; this.maxUniqueImages = maxUniqueImages; this.maxUploadCount = maxUploadCount; + this.minNonRevertPercentage = minNonRevertPercentage; } - public static LevelInfo from(int imagesUploaded, int uniqueImagesUsed) { - LevelInfo level = LEVEL_1; + public static LevelInfo from(int imagesUploaded, + int uniqueImagesUsed, + int nonRevertRate) { + LevelInfo level = LEVEL_15; for (LevelInfo levelInfo : LevelInfo.values()) { - if (imagesUploaded > levelInfo.maxUploadCount && uniqueImagesUsed > levelInfo.maxUniqueImages) { + if (imagesUploaded < levelInfo.maxUploadCount + || uniqueImagesUsed < levelInfo.maxUniqueImages + || nonRevertRate < levelInfo.minNonRevertPercentage ) { level = levelInfo; return level; } @@ -57,10 +69,6 @@ public int getLevelNumber() { return levelNumber; } - public void setLevelStyle(int levelStyle) { - this.levelStyle = levelStyle; - } - public int getMaxUniqueImages() { return maxUniqueImages; } @@ -68,6 +76,10 @@ public int getMaxUniqueImages() { public int getMaxUploadCount() { return maxUploadCount; } + + public int getMinNonRevertPercentage(){ + return minNonRevertPercentage; + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index d0fb628e3e..6ece0523b3 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -6,6 +6,7 @@ import android.accounts.AccountManager; import android.app.Activity; import android.app.ProgressDialog; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -440,4 +441,9 @@ public void afterTextChanged(Editable editable) { loginButton.setEnabled(enabled); } } + + public static void startYourself(Context context) { + Intent intent = new Intent(context, LoginActivity.class); + context.startActivity(intent); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index ad9a0bda40..fc876fe106 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -88,4 +88,10 @@ public Completable clearAllAccounts() { .map(a -> accountManager.removeAccount(a, null, null).getResult())) .doOnComplete(() -> currentAccount = null); } + + public void forceLogin(Context context) { + if (context != null) { + LoginActivity.startYourself(context); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 5056a0e12b..e8a57a5ba9 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -651,6 +651,37 @@ public Single getAchievements(String userName) { } + /** + * This takes userName as input, which is then used to fetch the no of images deleted + * using OkHttp and JavaRx. This function return JSONObject + * @param userName + * @return + */ + @NonNull + @Override + public Single getRevertCount(String userName){ + final String fetchRevertCountUrlTemplate = + wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py"; + return Single.fromCallable(() -> { + String url = String.format( + Locale.ENGLISH, + fetchRevertCountUrlTemplate, + new PageTitle(userName).getText()); + HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); + urlBuilder.addQueryParameter("user", userName); + urlBuilder.addQueryParameter("fetch","deletedUploads"); + Log.i("url", urlBuilder.toString()); + Request request = new Request.Builder() + .url(urlBuilder.toString()) + .build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + String jsonData = response.body().string(); + JSONObject jsonRevertObject = new JSONObject(jsonData); + return jsonRevertObject; + }); + } + private Date parseMWDate(String mwDate) { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC try { diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index 5cb5c45913..6622ce9d4d 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -80,6 +80,9 @@ public interface MediaWikiApi { @NonNull Single getAchievements(String userName); + @NonNull + Single getRevertCount(String userName); + interface ProgressListener { void onProgress(long transferred, long total); } diff --git a/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml b/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml new file mode 100644 index 0000000000..e3f68ad02d --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_achievements.xml b/app/src/main/res/layout/activity_achievements.xml index f289eafe8d..293234f090 100644 --- a/app/src/main/res/layout/activity_achievements.xml +++ b/app/src/main/res/layout/activity_achievements.xml @@ -71,9 +71,20 @@ android:layout_height="wrap_content" android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" + android:id="@+id/images_upload_text_param" android:layout_marginTop="@dimen/achievements_activity_margin_vertical" android:text="@string/images_uploaded" /> + + + + @@ -153,11 +175,22 @@ style="?android:textAppearanceMedium" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:id="@+id/images_used_by_wiki_text" android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/achievements_activity_margin_vertical" android:text="@string/images_used_by_wiki" /> + + LEVEL Images Uploaded Images Not Reverted - Images Used By Wiki + Images Used Share your achievements with your friends! Your level increases as you meet these requirements. Items in the "statistics" section do not count towards your level. + minimum required: + The number of images you have uploaded to Commons, via any upload software + The percentage of images you have uploaded to Commons that were not deleted + The number of images you have uploaded to Commons that were used in Wikimedia articles + Login session expired, please log in again.